├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ └── installMediaWiki.sh ├── .gitignore ├── .stylelintrc.json ├── Gruntfile.js ├── LICENSE ├── Makefile ├── README.md ├── WikibaseRdf.alias.php ├── codecov.yml ├── composer.json ├── docs └── rest.md ├── extension.json ├── i18n ├── ar.json ├── bn.json ├── de.json ├── en.json ├── fr.json ├── he.json ├── hi.json ├── hu.json ├── ia.json ├── id.json ├── it.json ├── ja.json ├── ko.json ├── lb.json ├── lfn.json ├── lv.json ├── mk.json ├── nl.json ├── pt.json ├── qqq.json ├── ru.json ├── sl.json ├── sms.json ├── sv.json ├── tr.json ├── zh-hans.json └── zh-hant.json ├── package.json ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml.dist ├── psalm-baseline.xml ├── psalm.xml ├── resources ├── ext.wikibase.rdf.js └── ext.wikibase.rdf.less ├── src ├── Application │ ├── AllMappingsLookup.php │ ├── EntityMappingsAuthorizer.php │ ├── Mapping.php │ ├── MappingList.php │ ├── MappingListAndId.php │ ├── MappingRepository.php │ ├── ObjectValidator.php │ ├── Predicate.php │ ├── PredicateList.php │ ├── SaveMappings │ │ ├── SaveMappingsPresenter.php │ │ └── SaveMappingsUseCase.php │ └── WikibaseUrlObjectValidator.php ├── DataAccess │ ├── CombiningMappingPredicatesLookup.php │ ├── LocalSettingsMappingPredicatesLookup.php │ ├── MappingPredicatesLookup.php │ ├── PageContentFetcher.php │ ├── PredicatesDeserializer.php │ ├── PredicatesTextValidator.php │ └── WikiMappingPredicatesLookup.php ├── EntryPoints │ ├── MediaWikiHooks.php │ └── Rest │ │ ├── GetAllMappingsApi.php │ │ ├── GetMappingsApi.php │ │ └── SaveMappingsApi.php ├── MappingListSerializer.php ├── Persistence │ ├── ContentSlotMappingRepository.php │ ├── EntityContentRepository.php │ ├── InMemoryEntityContentRepository.php │ ├── InMemoryMappingRepository.php │ ├── SlotEntityContentRepository.php │ ├── SqlAllMappingsLookup.php │ └── UserBasedEntityMappingsAuthorizer.php ├── Presentation │ ├── HtmlMappingsPresenter.php │ ├── HtmlPredicatesPresenter.php │ ├── MappingsPresenter.php │ ├── PredicatesPresenter.php │ ├── RDF │ │ ├── MappingRdfBuilder.php │ │ ├── MultiEntityRdfBuilder.php │ │ └── PropertyMappingPrefixBuilder.php │ └── RestSaveMappingsPresenter.php ├── SpecialMappingPredicates.php └── WikibaseRdfExtension.php └── tests ├── Application ├── MappingListUnitTest.php ├── MappingTest.php ├── PredicateListTest.php ├── PredicateTest.php ├── SaveMappings │ └── SaveMappingsUseCaseTest.php └── WikibaseUrlObjectValidatorTest.php ├── DataAccess ├── CombiningMappingPredicatesLookupTest.php ├── LocalSettingsMappingPredicatesLookupTest.php ├── PredicatesDeserializerTest.php ├── PredicatesTextValidatorTest.php └── WikiMappingPredicatesLookupTest.php ├── EntryPoints ├── MappingUiLoadingTest.php └── Rest │ ├── GetMappingsApiTest.php │ └── SaveMappingsApiTest.php ├── MappingListSerializerTest.php ├── Persistence ├── ContentSlotMappingRepositoryTest.php ├── InMemoryMappingRepositoryUnitTest.php ├── SlotEntityContentRepositoryTest.php ├── SqlAllMappingsLookupPerformanceTest.php ├── SqlAllMappingsLookupTest.php └── UserBasedEntityMappingsAuthorizerTest.php ├── Presentation ├── HtmlMappingsPresenterTest.php ├── HtmlPredicatesPresenterTest.php └── RDF │ └── PropertyMappingPrefixBuilderTest.php ├── TestDoubles ├── FailingEntityMappingsAuthorizer.php ├── FailingObjectValidator.php ├── PermissionDeniedMappingRepository.php ├── SpySaveMappingsPresenter.php ├── SucceedingEntityMappingsAuthorizer.php ├── SucceedingObjectValidator.php └── ThrowingMappingRepository.php └── WikibaseRdfIntegrationTest.php /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "wikimedia/client-es6", 5 | "wikimedia/mediawiki", 6 | "wikimedia/jquery" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "es6": true 11 | }, 12 | "rules": { 13 | "no-jquery/no-parse-html-literal": "warn", 14 | "no-jquery/no-global-selector": "warn", 15 | "no-jquery/no-class-state": "warn", 16 | "mediawiki/msg-doc": "warn" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: "PHPUnit: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - mw: 'REL1_43' 16 | php: 8.1 17 | experimental: false 18 | - mw: 'REL1_43' 19 | php: 8.2 20 | experimental: false 21 | - mw: 'REL1_43' 22 | php: 8.3 23 | experimental: false 24 | - mw: 'master' 25 | php: 8.4 26 | experimental: true 27 | 28 | runs-on: ubuntu-latest 29 | 30 | defaults: 31 | run: 32 | working-directory: mediawiki 33 | 34 | steps: 35 | - name: Setup PHP 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ matrix.php }} 39 | extensions: mbstring, intl 40 | tools: composer 41 | 42 | - name: Cache MediaWiki 43 | id: cache-mediawiki 44 | uses: actions/cache@v4 45 | with: 46 | path: | 47 | mediawiki 48 | !mediawiki/extensions/ 49 | !mediawiki/vendor/ 50 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4 51 | 52 | - name: Cache Composer cache 53 | uses: actions/cache@v4 54 | with: 55 | path: ~/.composer/cache 56 | key: composer-php${{ matrix.php }} 57 | 58 | - uses: actions/checkout@v4 59 | with: 60 | path: EarlyCopy 61 | 62 | - name: Install MediaWiki 63 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 64 | working-directory: ~ 65 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF 66 | 67 | - uses: actions/checkout@v4 68 | with: 69 | path: mediawiki/extensions/WikibaseRDF 70 | 71 | - name: Composer allow-plugins 72 | run: composer config --no-plugins allow-plugins.composer/installers true 73 | 74 | - run: composer update 75 | 76 | - name: Run update.php 77 | run: php maintenance/update.php --quick 78 | 79 | - name: Run PHPUnit 80 | run: php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/ 81 | if: matrix.mw != 'master' 82 | 83 | - name: Run PHPUnit with code coverage 84 | run: php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/ --coverage-clover coverage.xml 85 | if: matrix.mw != 'master' 86 | 87 | - name: Upload code coverage 88 | run: bash <(curl -s https://codecov.io/bash) 89 | if: matrix.mw == 'master' 90 | 91 | 92 | 93 | 94 | PHPStan: 95 | name: "PHPStan" 96 | 97 | strategy: 98 | matrix: 99 | include: 100 | - mw: 'REL1_43' 101 | php: '8.3' 102 | 103 | runs-on: ubuntu-latest 104 | 105 | defaults: 106 | run: 107 | working-directory: mediawiki 108 | 109 | steps: 110 | - name: Setup PHP 111 | uses: shivammathur/setup-php@v2 112 | with: 113 | php-version: ${{ matrix.php }} 114 | extensions: mbstring 115 | tools: composer, cs2pr 116 | 117 | - name: Cache MediaWiki 118 | id: cache-mediawiki 119 | uses: actions/cache@v4 120 | with: 121 | path: | 122 | mediawiki 123 | mediawiki/extensions/ 124 | mediawiki/vendor/ 125 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4 126 | 127 | - name: Cache Composer cache 128 | uses: actions/cache@v4 129 | with: 130 | path: ~/.composer/cache 131 | key: composer_static_analysis 132 | 133 | - uses: actions/checkout@v4 134 | with: 135 | path: EarlyCopy 136 | 137 | - name: Install MediaWiki 138 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 139 | working-directory: ~ 140 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF 141 | 142 | - uses: actions/checkout@v4 143 | with: 144 | path: mediawiki/extensions/WikibaseRDF 145 | 146 | - name: Composer allow-plugins 147 | run: composer config --no-plugins allow-plugins.composer/installers true 148 | 149 | - run: composer update 150 | 151 | - name: Composer install 152 | run: cd extensions/WikibaseRDF && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader 153 | 154 | - name: PHPStan 155 | run: cd extensions/WikibaseRDF && php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress | cs2pr 156 | 157 | 158 | 159 | 160 | Psalm: 161 | name: "Psalm: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 162 | 163 | strategy: 164 | matrix: 165 | include: 166 | - mw: 'REL1_43' 167 | php: '8.3' 168 | 169 | runs-on: ubuntu-latest 170 | 171 | defaults: 172 | run: 173 | working-directory: mediawiki 174 | 175 | steps: 176 | - name: Setup PHP 177 | uses: shivammathur/setup-php@v2 178 | with: 179 | php-version: ${{ matrix.php }} 180 | extensions: mbstring 181 | tools: composer, cs2pr 182 | 183 | - name: Cache MediaWiki 184 | id: cache-mediawiki 185 | uses: actions/cache@v4 186 | with: 187 | path: | 188 | mediawiki 189 | mediawiki/extensions/ 190 | mediawiki/vendor/ 191 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4 192 | 193 | - name: Cache Composer cache 194 | uses: actions/cache@v4 195 | with: 196 | path: ~/.composer/cache 197 | key: composer_static_analysis 198 | 199 | - uses: actions/checkout@v4 200 | with: 201 | path: EarlyCopy 202 | 203 | - name: Install MediaWiki 204 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 205 | working-directory: ~ 206 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF 207 | 208 | - uses: actions/checkout@v4 209 | with: 210 | path: mediawiki/extensions/WikibaseRDF 211 | 212 | - name: Composer allow-plugins 213 | run: composer config --no-plugins allow-plugins.composer/installers true 214 | 215 | - run: composer update 216 | 217 | - name: Composer install 218 | run: cd extensions/WikibaseRDF && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader 219 | 220 | - name: Psalm 221 | run: cd extensions/WikibaseRDF && pwd && php vendor/bin/psalm --config=psalm.xml --shepherd --stats 222 | 223 | 224 | 225 | 226 | code-style: 227 | name: "Code style: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 228 | 229 | strategy: 230 | matrix: 231 | include: 232 | - mw: 'REL1_43' 233 | php: '8.3' 234 | 235 | runs-on: ubuntu-latest 236 | 237 | defaults: 238 | run: 239 | working-directory: mediawiki/extensions/WikibaseRDF 240 | 241 | steps: 242 | - name: Setup PHP 243 | uses: shivammathur/setup-php@v2 244 | with: 245 | php-version: ${{ matrix.php }} 246 | extensions: mbstring, intl, php-ast 247 | tools: composer 248 | 249 | - name: Cache MediaWiki 250 | id: cache-mediawiki 251 | uses: actions/cache@v4 252 | with: 253 | path: | 254 | mediawiki 255 | !mediawiki/extensions/ 256 | !mediawiki/vendor/ 257 | key: mw_static_analysis 258 | 259 | - name: Cache Composer cache 260 | uses: actions/cache@v4 261 | with: 262 | path: ~/.composer/cache 263 | key: composer_static_analysis 264 | 265 | - uses: actions/checkout@v4 266 | with: 267 | path: EarlyCopy 268 | 269 | - name: Install MediaWiki 270 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 271 | working-directory: ~ 272 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF 273 | 274 | - uses: actions/checkout@v4 275 | with: 276 | path: mediawiki/extensions/WikibaseRDF 277 | 278 | - name: Composer install 279 | run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader 280 | 281 | - run: vendor/bin/phpcs -p -s 282 | 283 | linting: 284 | name: "Linting" 285 | 286 | runs-on: ubuntu-latest 287 | 288 | steps: 289 | - uses: actions/checkout@v4 290 | - uses: actions/setup-node@v4 291 | with: 292 | node-version: 22 293 | - run: | 294 | echo "::remove-matcher owner=eslint-compact::" 295 | echo "::remove-matcher owner=eslint-stylish::" 296 | - run: npm install 297 | - run: npm run test 298 | -------------------------------------------------------------------------------- /.github/workflows/installMediaWiki.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | MW_BRANCH=$1 4 | EXTENSION_NAME=$2 5 | 6 | wget "https://github.com/wikimedia/mediawiki/archive/refs/heads/$MW_BRANCH.tar.gz" -nv 7 | 8 | tar -zxf $MW_BRANCH.tar.gz 9 | mv mediawiki-$MW_BRANCH mediawiki 10 | 11 | cd mediawiki 12 | 13 | composer install 14 | php maintenance/install.php --dbtype sqlite --dbuser root --dbname mw --dbpath $(pwd) --pass AdminPassword WikiName AdminUser 15 | 16 | echo 'error_reporting(E_ALL| E_STRICT);' >> LocalSettings.php 17 | echo 'ini_set("display_errors", 1);' >> LocalSettings.php 18 | echo '$wgShowExceptionDetails = true;' >> LocalSettings.php 19 | echo '$wgShowDBErrorBacktrace = true;' >> LocalSettings.php 20 | echo '$wgDevelopmentWarnings = true;' >> LocalSettings.php 21 | 22 | echo "wfLoadExtension( 'WikibaseRepository', __DIR__ . '/extensions/Wikibase/extension-repo.json' );" >> LocalSettings.php 23 | echo 'require_once __DIR__ . "/extensions/Wikibase/repo/ExampleSettings.php";' >> LocalSettings.php 24 | 25 | echo 'wfLoadExtension( "'$EXTENSION_NAME'" );' >> LocalSettings.php 26 | 27 | cat <> composer.local.json 28 | { 29 | "extra": { 30 | "merge-plugin": { 31 | "merge-dev": true, 32 | "include": [ 33 | "extensions/Wikibase/composer.json", 34 | "extensions/$EXTENSION_NAME/composer.json" 35 | ] 36 | } 37 | } 38 | } 39 | EOT 40 | 41 | cd extensions 42 | git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/Wikibase --branch=$MW_BRANCH --recurse-submodules -j8 43 | 44 | cd Wikibase 45 | git submodule set-url view/lib/wikibase-serialization https://github.com/wmde/WikibaseSerializationJavaScript.git 46 | git submodule set-url view/lib/wikibase-data-values https://github.com/wmde/DataValuesJavaScript.git 47 | git submodule set-url view/lib/wikibase-data-model https://github.com/wmde/WikibaseDataModelJavaScript.git 48 | git submodule sync && git submodule init && git submodule update --recursive 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | composer.lock 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-wikimedia", 3 | "rules": { 4 | "unit-disallowed-list": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = function ( grunt ) { 3 | const conf = grunt.file.readJSON( 'extension.json' ); 4 | 5 | grunt.loadNpmTasks( 'grunt-banana-checker' ); 6 | grunt.loadNpmTasks( 'grunt-eslint' ); 7 | grunt.loadNpmTasks( 'grunt-stylelint' ); 8 | 9 | grunt.initConfig( { 10 | eslint: { 11 | options: { 12 | cache: true 13 | }, 14 | all: '.' 15 | }, 16 | stylelint: { 17 | all: [ 18 | '**/*.{css,less}', 19 | '!**/coverage/**', 20 | '!node_modules/**', 21 | '!vendor/**' 22 | ] 23 | }, 24 | banana: conf.MessagesDirs 25 | } ); 26 | 27 | grunt.registerTask( 'test', [ 'eslint', 'stylelint', 'banana' ] ); 28 | grunt.registerTask( 'default', 'test' ); 29 | }; 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ci test cs phpunit phpcs stan psalm 2 | 3 | ci: test cs 4 | test: phpunit 5 | cs: phpcs stan psalm 6 | 7 | phpunit: 8 | php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist 9 | 10 | perf: 11 | php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist --group Performance 12 | 13 | phpcs: 14 | vendor/bin/phpcs -p -s --standard=$(shell pwd)/phpcs.xml 15 | 16 | stan: 17 | vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G 18 | 19 | stan-baseline: 20 | vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G --generate-baseline 21 | 22 | psalm: 23 | vendor/bin/psalm --config=psalm.xml 24 | 25 | psalm-baseline: 26 | vendor/bin/psalm --config=psalm.xml --set-baseline=psalm-baseline.xml 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikibase RDF 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ProfessionalWiki/WikibaseRDF/ci.yml?branch=master)](https://github.com/ProfessionalWiki/WikibaseRDF/actions?query=workflow%3ACI) 4 | [![Type Coverage](https://shepherd.dev/github/ProfessionalWiki/WikibaseRDF/coverage.svg)](https://shepherd.dev/github/ProfessionalWiki/WikibaseRDF) 5 | [![Psalm level](https://shepherd.dev/github/ProfessionalWiki/WikibaseRDF/level.svg)](psalm.xml) 6 | [![Latest Stable Version](https://poser.pugx.org/professional-wiki/wikibase-rdf/v/stable)](https://packagist.org/packages/professional-wiki/wikibase-rdf) 7 | [![Download count](https://poser.pugx.org/professional-wiki/wikibase-rdf/downloads)](https://packagist.org/packages/professional-wiki/wikibase-rdf) 8 | [![License](https://img.shields.io/packagist/l/professional-wiki/wikibase-rdf)](LICENSE) 9 | 10 | [Wikibase] extension that allows defining RDF mappings for Wikibase Entities. 11 | 12 | [Professional.Wiki] created and maintains Wikibase RDF. We provide [Wikibase hosting], [Wikibase development] and [Wikibase consulting]. 13 | 14 | The [Wikibase Stakeholder Group] concieved and funded the extension. 15 | 16 | **Table of Contents** 17 | 18 | - [Usage](#usage) 19 | * [REST API](#rest-api) 20 | - [Installation](#installation) 21 | - [PHP Configuration](#php-configuration) 22 | - [Development](#development) 23 | - [Release notes](#release-notes) 24 | 25 | ## Usage 26 | 27 | When the extension is enabled, Item and Property pages show a "Mapping to other ontologies" section. 28 | This section is located in between the "In more languages" and "Statements" sections. 29 | 30 | Property page with a mapping 31 | 32 | Users with editing permissions can add, edit or remove mappings. 33 | 34 | A mapping consists of a predicate and a URL. The predicate can only be one out of a preconfigured set of values. The URL has to be a valid URL. 35 | 36 | Mapping editing UI 37 | 38 | Mapping predicates can be configured via the `MediaWiki:MappingPredicates` page by anyone with interface-admin permissions. 39 | You can also configure mapping predicates via [PHP Configuration](#php-configuration). 40 | 41 | Mapping predicates configuration page 42 | 43 | Editing mapping predicates via the configuration page 44 | 45 | ### REST API 46 | 47 | This extension provides REST API endpoints for getting and setting the RDF mappings for a Wikibase entity. 48 | 49 | For more information, refer to the [REST API documentation](docs/rest.md). 50 | 51 | ## Installation 52 | 53 | Platform requirements: 54 | 55 | * [PHP] 8.0 or later (tested up to 8.1) 56 | * [MediaWiki] 1.43 or later (tested up to 1.43) 57 | * [Wikibase] 1.43 or later (tested up to 1.43) 58 | 59 | The recommended way to install Wikibase RDF is using [Composer] with 60 | [MediaWiki's built-in support for Composer][Composer install]. 61 | 62 | On the commandline, go to your wikis root directory. Then run these two commands: 63 | 64 | ```shell script 65 | COMPOSER=composer.local.json composer require --no-update professional-wiki/wikibase-rdf:~1.0 66 | ``` 67 | ```shell script 68 | composer update professional-wiki/wikibase-rdf --no-dev -o 69 | ``` 70 | 71 | Then enable the extension by adding the following to the bottom of your wikis [LocalSettings.php] file: 72 | 73 | ```php 74 | wfLoadExtension( 'WikibaseRDF' ); 75 | ``` 76 | 77 | You can verify the extension was enabled successfully by opening your wikis Special:Version page in your browser. 78 | 79 | ## PHP Configuration 80 | 81 | Configuration can be changed via [LocalSettings.php]. 82 | 83 | ### Allowed predicates 84 | 85 | List of allowed predicates. 86 | 87 | Variable: `$wgWikibaseRdfPredicates` 88 | 89 | Default: `[]` 90 | 91 | Example: 92 | 93 | ```php 94 | $wgWikibaseRdfPredicates = [ 95 | 'owl:sameAs', 96 | 'owl:SymmetricProperty', 97 | 'rdfs:subClassOf', 98 | 'rdfs:subPropertyOf', 99 | ]; 100 | ``` 101 | 102 | You can also configure what predicates are allowed via the `MediaWiki:MappingPredicates` page. 103 | 104 | ## Development 105 | 106 | To ensure the dev dependencies get installed, have this in your `composer.local.json`: 107 | 108 | ```json 109 | { 110 | "require": { 111 | "vimeo/psalm": "^4", 112 | "phpstan/phpstan": "^1.4.9" 113 | }, 114 | "extra": { 115 | "merge-plugin": { 116 | "include": [ 117 | "extensions/WikibaseRDF/composer.json" 118 | ] 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ### Running tests and CI checks 125 | 126 | You can use the `Makefile` by running make commands in the `WikibaseRDF` directory. 127 | 128 | * `make ci`: Run everything 129 | * `make test`: Run all tests 130 | * `make cs`: Run all style checks and static analysis 131 | 132 | Alternatively, you can execute commands from the MediaWiki root directory: 133 | 134 | * PHPUnit: `php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/` 135 | * Style checks: `vendor/bin/phpcs -p -s --standard=extensions/WikibaseRDF/phpcs.xml` 136 | * PHPStan: `vendor/bin/phpstan analyse --configuration=extensions/WikibaseRDF/phpstan.neon --memory-limit=2G` 137 | * Psalm: `php vendor/bin/psalm --config=extensions/WikibaseRDF/psalm.xml` 138 | 139 | ## Release notes 140 | 141 | ### Version 1.1.0 - 2022-11-25 142 | 143 | * Added translations for various languages 144 | * Added notification about SPARQL store behavior that shows on first edit 145 | 146 | ### Version 1.0.0 - 2022-10-04 147 | 148 | Initial release for Wikibase 1.37 with these features: 149 | 150 | * Ability to add mappings to Items and Properties via an on-page UI 151 | * Inclusion of mappings in the RDF output 152 | * Configurable relationships (predicates), including configuration UI on `MediaWiki:MappingPredicates` 153 | * API endpoint to retrieve or update the mappings for an Entity 154 | * API endpoint to retrieve all mappings defined on the wiki 155 | * TranslateWiki integration 156 | * Support for PHP 8.0 and 8.1 157 | 158 | [Professional.Wiki]: https://professional.wiki 159 | [Wikibase]: https://wikibase.consulting/what-is-wikibase/ 160 | [Wikibase hosting]: https://professional.wiki/en/hosting/wikibase 161 | [Wikibase development]: https://professional.wiki/en/wikibase-software-development 162 | [Wikibase consulting]: https://wikibase.consulting/ 163 | [MediaWiki]: https://www.mediawiki.org 164 | [PHP]: https://www.php.net 165 | [Composer]: https://getcomposer.org 166 | [Composer install]: https://professional.wiki/en/articles/installing-mediawiki-extensions-with-composer 167 | [LocalSettings.php]: https://www.pro.wiki/help/mediawiki-localsettings-php-guide 168 | [Wikibase Stakeholder Group]:https://wbstakeholder.group/ 169 | -------------------------------------------------------------------------------- /WikibaseRdf.alias.php: -------------------------------------------------------------------------------- 1 | [ 'MappingPredicates' ] 14 | ]; 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "extensions/WikibaseRDF/::" 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "professional-wiki/wikibase-rdf", 3 | "type": "mediawiki-extension", 4 | "description": "Wikibase extension that allows defining RDF mappings for Wikibase Entities", 5 | "keywords": [ 6 | "MediaWiki", 7 | "Wikibase", 8 | "RDF" 9 | ], 10 | "homepage": "https://professional.wiki/en/extension/wikibase-rdf", 11 | "license": "GPL-2.0-or-later", 12 | "authors": [ 13 | { 14 | "name": "Professional.Wiki", 15 | "email": "info@Professional.Wiki", 16 | "homepage": "https://Professional.Wiki" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/ProfessionalWiki/WikibaseRDF/issues", 21 | "source": "https://github.com/ProfessionalWiki/WikibaseRDF" 22 | }, 23 | "require": { 24 | "php": ">=8.1", 25 | "composer/installers": "^2|^1.0.1" 26 | }, 27 | "require-dev": { 28 | "vimeo/psalm": "^4.30.0", 29 | "phpstan/phpstan": "^1.4.8", 30 | "mediawiki/mediawiki-codesniffer": "^46.0.0" 31 | }, 32 | "extra": { 33 | "installer-name": "WikibaseRDF" 34 | }, 35 | "config": { 36 | "allow-plugins": { 37 | "composer/installers": true, 38 | "dealerdirect/phpcodesniffer-composer-installer": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/rest.md: -------------------------------------------------------------------------------- 1 | # Wikibase RDF REST API 2 | 3 | **Table of Contents** 4 | 5 | - [Get Entity Mappings](#get-entity-mappings) 6 | - [Save Entity Mappings](#save-entity-mappings) 7 | - [Get All Mappings](#get-all-mappings) 8 | 9 | ## Get Entity Mappings 10 | 11 | Gets the mappings for a specific Wikibase entity. 12 | 13 | Route: `/wikibase-rdf/v1/mappings/{entity_id}` 14 | 15 | Method: `GET` 16 | 17 | Example for retrieving all RDF mappings for entity `Item:Q1`: 18 | 19 | ```shell 20 | curl "http://localhost:8484/rest.php/wikibase-rdf/v1/mappings/Q1" 21 | ``` 22 | 23 | ### Request parameters 24 | 25 | **Query** 26 | 27 | | parameter | required | example | description | 28 | |-------------|----------|-----------------------------------------------|---------------------------------------------| 29 | | `entity_id` | yes | `Q1` for `Item:Q1`
`P1` for `Property:P1` | The entity ID, without the namespace prefix | 30 | 31 | **Body** 32 | 33 | None 34 | 35 | ### Responses 36 | 37 | #### 200: Success 38 | 39 | Mappings were retrieved or no mappings were found. 40 | 41 | **Example with mappings** 42 | 43 | ```json 44 | { 45 | "mappings": [ 46 | { 47 | "predicate": "owl:sameAs", 48 | "object": "http://www.w3.org/2000/01/rdf-schema#subClassOf" 49 | }, 50 | { 51 | "predicate": "rdfs:subClassOf", 52 | "object": "foo:Bar" 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | **Example without mappings** 59 | 60 | ```json 61 | { 62 | "mappings": [] 63 | } 64 | ``` 65 | 66 | #### 400: Invalid Entity ID 67 | 68 | The Entity ID format is invalid. 69 | 70 | **Example with invalid Entity ID `ABC123`** 71 | 72 | ```json 73 | { 74 | "messageTranslations": { 75 | "en": "The entity ID you specified is invalid" 76 | }, 77 | "httpCode": 400, 78 | "httpReason": "Bad Request" 79 | } 80 | ``` 81 | 82 | ### Response schema 83 | 84 | | key | type | description | 85 | |-------------------------|--------|---------------------------| 86 | | `mappings` | array | List of mapping objects | 87 | | `mappings[i].predicate` | string | Predicate for mapping `i` | 88 | | `mappings[i].object` | string | Object for mapping `i` | 89 | 90 | ## Save Entity Mappings 91 | 92 | Sets the mappings for a specific Wikibase entity. 93 | 94 | Route: `/wikibase-rdf/v1/mappings/{entity_id}` 95 | 96 | Method: `POST` 97 | 98 | Example for saving all RDF mappings for existing entity `Item:Q1`: 99 | 100 | ```shell 101 | curl -X POST -H 'Content-Type: application/json' "http://localhost:8484/rest.php/wikibase-rdf/v1/mappings/Q1" \ 102 | -d '{"mappings": [{"predicate": "owl:sameAs", "object": "http://www.w3.org/2000/01/rdf-schema#subClassOf"}, {"predicate": "rdfs:subClassOf", "object": "foo:Bar"}]}' 103 | ``` 104 | 105 | ### Request parameters 106 | 107 | **Query** 108 | 109 | | parameter | required | example | description | 110 | |-------------|----------|-----------------------------------------------|---------------------------------------------| 111 | | `entity_id` | yes | `Q1` for `Item:Q1`
`P1` for `Property:P1` | The entity ID, without the namespace prefix | 112 | 113 | **Body** 114 | 115 | The request body must be a JSON object with a "mappings" key containing an array. Each item in the array should contain: 116 | 117 | | key | type | description | 118 | |-----------------|--------|---------------------------| 119 | | `mappings[i].predicate` | string | Predicate for mapping `i` | 120 | | `mappings[i].object` | string | Object for mapping `i` | 121 | 122 | ### Response 123 | 124 | #### 200: Success 125 | 126 | A successful save will have an empty response body. 127 | 128 | #### 400: Invalid Entity ID 129 | 130 | ```json 131 | { 132 | "messageTranslations": { 133 | "en": "The entity ID you specified is invalid" 134 | }, 135 | "httpCode": 400, 136 | "httpReason": "Bad Request" 137 | } 138 | ``` 139 | 140 | #### 400: Invalid Mappings 141 | 142 | Example containing an incomplete mapping: 143 | 144 | ```json 145 | { 146 | "invalidMappings": [ 147 | { 148 | "predicate": "rdfs:subClassOf", 149 | "object": "" 150 | } 151 | ], 152 | "messageTranslations": { 153 | "en": "Some of the mappings you specified are invalid" 154 | }, 155 | "httpCode": 400, 156 | "httpReason": "Bad Request" 157 | } 158 | ``` 159 | 160 | #### 403: Permission Denied 161 | 162 | ```json 163 | { 164 | "messageTranslations": { 165 | "en": "You do not have permission to edit this entity's mappings" 166 | }, 167 | "httpCode": 403, 168 | "httpReason": "Forbidden" 169 | } 170 | ``` 171 | 172 | #### 500: Save Failed 173 | 174 | Indicates a failure unrelated to the request data. 175 | 176 | ```json 177 | { 178 | "messageTranslations": { 179 | "en": "Save failed" 180 | }, 181 | "httpCode": 500, 182 | "httpReason": "Internal Server Error" 183 | } 184 | ``` 185 | 186 | ### Response schema 187 | 188 | None for successful requests. 189 | 190 | Requests containing invalid mappings will respond with the invalid mappings: 191 | 192 | | key | type | description | 193 | |--------------------------------|--------|-------------------------------------| 194 | | `invalidMappings` | array | List of invalid mappings | 195 | | `invalidMappings[i].predicate` | string | Predicate attempted for mapping `i` | 196 | | `invalidMappings[i].object` | string | Object attempted for mapping `i` | 197 | 198 | ## Get All Mappings 199 | 200 | Gets the mappings for all Wikibase entities. 201 | 202 | Route: `/wikibase-rdf/v1/mappings` 203 | 204 | Method: `GET` 205 | 206 | Example for retrieving all RDF mappings for all entities: 207 | 208 | ```shell 209 | curl "http://localhost:8484/rest.php/wikibase-rdf/v1/mappings" 210 | ``` 211 | 212 | ### Request parameters 213 | 214 | None 215 | 216 | ### Responses 217 | 218 | #### 200: Success 219 | 220 | Mappings were retrieved or no mappings were found. 221 | 222 | ```json 223 | { 224 | "mappings": { 225 | "Q1": [ 226 | { 227 | "predicate": "owl:sameAs", 228 | "object": "http://www.w3.org/2000/01/rdf-schema#subClassOf" 229 | }, 230 | { 231 | "predicate": "rdfs:subClassOf", 232 | "object": "foo:Bar" 233 | } 234 | ], 235 | "Q4": [ 236 | { 237 | "predicate": "owl:sameAs", 238 | "object": "bar:Baz" 239 | } 240 | ], 241 | "P3": [ 242 | { 243 | "predicate": "rdfs:subPropertyOf", 244 | "object": "http://www.w3.org/2000/01/rdf-schema#label" 245 | }, 246 | { 247 | "predicate": "foo:Bar", 248 | "object": "bar:Baz" 249 | } 250 | ] 251 | } 252 | } 253 | ``` 254 | 255 | ### Response schema 256 | 257 | | key | type | description | 258 | |----------------------------|--------|-----------------------------------------| 259 | | `mappings` | array | List of entities | 260 | | `mappings[n]` | array | List of mapping objects per entity | 261 | | `mappings[n][i].predicate` | string | Predicate for mapping `i` of entity `n` | 262 | | `mappings[n][i].object` | string | Object for mapping `i` of entity `n` | 263 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wikibase RDF", 3 | "type": "wikibase", 4 | 5 | "version": "2.0.0", 6 | 7 | "author": [ 8 | "[https://Professional.Wiki/ Professional.Wiki]", 9 | "[https://www.EntropyWins.wtf/mediawiki Jeroen De Dauw]" 10 | ], 11 | 12 | "license-name": "GPL-2.0-or-later", 13 | 14 | "url": "https://professional.wiki/en/extension/wikibase-rdf", 15 | 16 | "descriptionmsg": "wikibase-rdf-description", 17 | 18 | "requires": { 19 | "MediaWiki": ">= 1.43.0", 20 | "extensions": { 21 | "WikibaseRepository": "*" 22 | } 23 | }, 24 | 25 | "MessagesDirs": { 26 | "WikibaseRDF": [ 27 | "i18n" 28 | ] 29 | }, 30 | 31 | "AutoloadNamespaces": { 32 | "ProfessionalWiki\\WikibaseRDF\\": "src/", 33 | "ProfessionalWiki\\WikibaseRDF\\Tests\\": "tests/" 34 | }, 35 | 36 | "Hooks": { 37 | "AlternateEdit": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onAlternateEdit", 38 | "ArticleRevisionViewCustom": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onArticleRevisionViewCustom", 39 | "ContentHandlerDefaultModelFor": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onContentHandlerDefaultModelFor", 40 | "EditFilter": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onEditFilter", 41 | "GetPreferences": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onGetPreferences", 42 | "MediaWikiServices": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onMediaWikiServices", 43 | "OutputPageParserOutput": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onOutputPageParserOutput", 44 | "WikibaseRepoEntityTypes": "ProfessionalWiki\\WikibaseRDF\\EntryPoints\\MediaWikiHooks::onWikibaseRepoEntityTypes" 45 | }, 46 | 47 | "RestRoutes": [ 48 | { 49 | "path": "/wikibase-rdf/v1/mappings/{entity_id}", 50 | "method": [ "GET" ], 51 | "factory": "ProfessionalWiki\\WikibaseRDF\\WikibaseRdfExtension::getMappingsApiFactory" 52 | }, 53 | { 54 | "path": "/wikibase-rdf/v1/mappings/{entity_id}", 55 | "method": [ "POST" ], 56 | "factory": "ProfessionalWiki\\WikibaseRDF\\WikibaseRdfExtension::saveMappingsApiFactory" 57 | }, 58 | { 59 | "path": "/wikibase-rdf/v1/mappings", 60 | "method": [ "GET" ], 61 | "factory": "ProfessionalWiki\\WikibaseRDF\\WikibaseRdfExtension::getAllMappingsApiFactory" 62 | } 63 | ], 64 | 65 | "config": { 66 | "WikibaseRdfPredicates": { 67 | "description": "List of allowed predicates.", 68 | "value": [] 69 | } 70 | }, 71 | 72 | "SpecialPages": { 73 | "MappingPredicates": "ProfessionalWiki\\WikibaseRDF\\SpecialMappingPredicates" 74 | }, 75 | 76 | "ResourceFileModulePaths": { 77 | "localBasePath": "resources", 78 | "remoteExtPath": "WikibaseRDF/resources" 79 | }, 80 | 81 | "ResourceModules": { 82 | "ext.wikibase.rdf": { 83 | "scripts": [ 84 | "ext.wikibase.rdf.js" 85 | ], 86 | "styles": [ 87 | "ext.wikibase.rdf.less" 88 | ], 89 | "messages": [ 90 | "wikibase-rdf-mappings-toggler", 91 | "wikibase-rdf-mappings-action-save", 92 | "wikibase-rdf-property-edit-warning", 93 | "wikibase-rdf-property-edit-warning-version", 94 | "wikibase-rdf-property-edit-warning-acknowledge" 95 | ] 96 | } 97 | }, 98 | 99 | "ExtensionMessagesFiles": { 100 | "WikibaseRdfAlias": "WikibaseRdf.alias.php" 101 | }, 102 | 103 | "manifest_version": 2 104 | } 105 | -------------------------------------------------------------------------------- /i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Meno25", 5 | "Mohammed Qays" 6 | ] 7 | }, 8 | "wikibase-rdf-description": "يسمح بتحديد تعيينات RDF لكيانات Wikibase", 9 | "wikibase-rdf-mappings-toggler": "الربط مع الأنطولوجيات الأخرى", 10 | "wikibase-rdf-mappings-predicate-heading": "مسند", 11 | "wikibase-rdf-mappings-object-heading": "مسار", 12 | "wikibase-rdf-mappings-action-save": "حفظ", 13 | "wikibase-rdf-mappings-action-remove": "حذف", 14 | "wikibase-rdf-mappings-action-cancel": "إلغاء", 15 | "wikibase-rdf-mappings-action-edit": "عدّل", 16 | "wikibase-rdf-mappings-action-add": "إضافة خريطة", 17 | "wikibase-rdf-save-mappings-invalid-mappings": "بعض التعيينات التي حددتها غير صالحة", 18 | "wikibase-rdf-save-mappings-save-failed": "فشل الحفظ", 19 | "wikibase-rdf-entity-id-invalid": "معرف الكيان الذي حددته غير صالح", 20 | "wikibase-rdf-permission-denied": "ليس لديك إذن لتحرير تعيينات هذا الكيان", 21 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|مستندات}} غير صالح:\n $2 . لم يتم حفظ التغييرات التي أجريتها.", 22 | "wikibase-rdf-config-list-intro": "تحدد هذه الصفحة المسندات التي يمكن استخدامها في التعيينات إلى الأنطولوجيات الأخرى. هذه الوظيفة جزء من [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF extension].", 23 | "wikibase-rdf-config-list-heading": "مسموح بـ {{PLURAL:$1|مستندات}}:", 24 | "wikibase-rdf-config-list-empty": "لم يتم تعريف أي مسندات.", 25 | "wikibase-rdf-config-list-footer": "تم تعريفه في LocalSettings.php، ولا يمكن تعديله عبر هذه الصفحة.", 26 | "wikibase-rdf-property-edit-warning": "بالنقر فوق \"$1\" فإنك تقوم بإنشاء بيان دائم في المخزن الثلاثي. يرجى العلم بأن أي تعديلات لاحقة على الخاصية ستتطلب إعادة تعيين المخزن الثلاثي من أجل مسح التعيينات القديمة. لا ينطبق هذا على تعيينات العناصر", 27 | "wikibase-rdf-property-edit-warning-acknowledge": "لا تظهر هذه الرسالة مرة أخرى.", 28 | "special-mapping-predicates": "تعيين المسندات" 29 | } 30 | -------------------------------------------------------------------------------- /i18n/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "আজিজ", 5 | "আফতাবুজ্জামান" 6 | ] 7 | }, 8 | "wikibase-rdf-mappings-predicate-heading": "সম্পর্ক", 9 | "wikibase-rdf-mappings-object-heading": "ইউআরএল", 10 | "wikibase-rdf-mappings-action-save": "সংরক্ষণ", 11 | "wikibase-rdf-mappings-action-remove": "অপসারণ", 12 | "wikibase-rdf-mappings-action-cancel": "বাতিল", 13 | "wikibase-rdf-mappings-action-edit": "সম্পাদনা", 14 | "wikibase-rdf-save-mappings-save-failed": "সংরক্ষণ ব্যর্থ হয়েছে", 15 | "wikibase-rdf-permission-denied": "আপনার এই সত্তার মানচিত্রয়ন সম্পাদনা করার অনুমতি নেই", 16 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|বিধেয়}} বৈধ নয়:\n$2। আপনার পরিবর্তন সংরক্ষিত হয়নি।" 17 | } 18 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "CyberOne25", 5 | "GünniBusch", 6 | "Justman10000", 7 | "Kghbln" 8 | ] 9 | }, 10 | "wikibase-rdf-description": "Ermöglicht das Festlegen von RDF-Zuordnungen für Wikibase-Entitäten", 11 | "wikibase-rdf-mappings-toggler": "Zuordnungen zu anderen Ontologien", 12 | "wikibase-rdf-mappings-predicate-heading": "Aussage", 13 | "wikibase-rdf-mappings-object-heading": "URL", 14 | "wikibase-rdf-mappings-action-save": "speichern", 15 | "wikibase-rdf-mappings-action-remove": "entfernen", 16 | "wikibase-rdf-mappings-action-cancel": "abbrechen", 17 | "wikibase-rdf-mappings-action-edit": "bearbeiten", 18 | "wikibase-rdf-mappings-action-add": "Zuordnung hinzufügen", 19 | "wikibase-rdf-save-mappings-invalid-mappings": "Von {{#FORMAL:Dir|Ihnen}} vorgenommene Zuordnungen sind ungültig.", 20 | "wikibase-rdf-save-mappings-save-failed": "Speichern fehlgeschlagen", 21 | "wikibase-rdf-entity-id-invalid": "Die von {{#FORMAL:Dir|Ihnen}} angegebene Entitätskennung ist ungültig.", 22 | "wikibase-rdf-permission-denied": "{{#FORMAL:Du bist|Sie sind}} nicht berechtigt, die RDF-Zuordnungen zu dieser Entität zu bearbeiten.", 23 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Ungültiges RDF-Prädikat|Ungültige RDF-Prädikate}}:\n$2. Die Änderungen wurden nicht gespeichert.", 24 | "wikibase-rdf-config-list-intro": "Auf dieser Seite wird definiert, welche Prädikate bei Zuordnungen zu anderen Ontologien verwendet werden können. Diese Funktionalität ist Teil der [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF-Erweiterung].", 25 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Zulässiges RDF-Prädikat|Zulässige RDF-Prädikate}}:", 26 | "wikibase-rdf-config-list-empty": "Es wurden bislang keine RDF-Prädikate festgelegt.", 27 | "wikibase-rdf-config-list-footer": "Dies wird in der Konfigurationsdatei „LocalSettings.php“ festgelegt und kann nicht auf dieser Seite verändert werden.", 28 | "wikibase-rdf-property-edit-warning": "Wenn du auf \"$1\" klickst, erstellst du eine dauerhafte Aussage im Triplestore. Bitte beachte, dass alle nachfolgenden Änderungen an der Eigenschaft ein Zurücksetzen des Triplestore erfordern, um veraltete Zuordnungen zu löschen. Dies gilt nicht für Elementzuordnungen", 29 | "wikibase-rdf-property-edit-warning-acknowledge": "Zeige diese Meldung nicht mehr an.", 30 | "special-mapping-predicates": "RDF-Prädikate zuordnen" 31 | } 32 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Jeroen De Dauw" 5 | ] 6 | }, 7 | "wikibase-rdf-name": "Wikibase RDF", 8 | "wikibase-rdf-description": "Allows defining RDF mappings for Wikibase Entities", 9 | 10 | "wikibase-rdf-mappings-toggler": "Mapping to other ontologies", 11 | "wikibase-rdf-mappings-predicate-heading": "Predicate", 12 | "wikibase-rdf-mappings-object-heading": "URL", 13 | "wikibase-rdf-mappings-action-save": "save", 14 | "wikibase-rdf-mappings-action-remove": "remove", 15 | "wikibase-rdf-mappings-action-cancel": "cancel", 16 | "wikibase-rdf-mappings-action-edit": "edit", 17 | "wikibase-rdf-mappings-action-add": "add mapping", 18 | 19 | "wikibase-rdf-save-mappings-invalid-mappings": "Some of the mappings you specified are invalid", 20 | "wikibase-rdf-save-mappings-save-failed": "Save failed", 21 | "wikibase-rdf-entity-id-invalid": "The entity ID you specified is invalid", 22 | "wikibase-rdf-permission-denied": "You do not have permission to edit this entity's mappings", 23 | 24 | "wikibase-rdf-config-intro": "", 25 | "wikibase-rdf-config-invalid": "Invalid {{PLURAL:$1|predicate|predicates}}:\n$2. Your changes were not saved.", 26 | 27 | "wikibase-rdf-config-list-intro": "This page defines which predicates can be used in mappings to other ontologies. This functionality is part of the [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF extension].", 28 | "wikibase-rdf-config-list-heading": "Allowed {{PLURAL:$1|predicate|predicates}}:", 29 | "wikibase-rdf-config-list-empty": "No predicates are defined.", 30 | "wikibase-rdf-config-list-footer": "Defined in LocalSettings.php, cannot be modified via this page.", 31 | 32 | "wikibase-rdf-property-edit-warning": "By clicking \"$1\" you are creating a permanent statement in the triplestore. Please be advised that any subsequent edits to the property will require a reset of the triplestore in order to clear outdated mappings. This does not apply to item mappings", 33 | "wikibase-rdf-property-edit-warning-version": "wikibase-rdf-1", 34 | "wikibase-rdf-property-edit-warning-acknowledge": "Do not show this message again.", 35 | 36 | "special-mapping-predicates": "Mapping predicates" 37 | } 38 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eihel", 5 | "Gomoko", 6 | "JLTRY", 7 | "Kghbln", 8 | "Verdy p", 9 | "Wladek92" 10 | ] 11 | }, 12 | "wikibase-rdf-description": "Extension Wikibase qui permet de définir des correspondances RDF pour les entités de Wikibase", 13 | "wikibase-rdf-mappings-toggler": "Correspondance vers d’autres ontologies", 14 | "wikibase-rdf-mappings-predicate-heading": "Prédicat", 15 | "wikibase-rdf-mappings-object-heading": "URL", 16 | "wikibase-rdf-mappings-action-save": "enregistrer", 17 | "wikibase-rdf-mappings-action-remove": "retirer", 18 | "wikibase-rdf-mappings-action-cancel": "annuler", 19 | "wikibase-rdf-mappings-action-edit": "modifier", 20 | "wikibase-rdf-mappings-action-add": "ajouter une correspondance", 21 | "wikibase-rdf-save-mappings-invalid-mappings": "Certaines des correspondances que vous avez spécifiées ne sont pas valides", 22 | "wikibase-rdf-save-mappings-save-failed": "Échec de la sauvegarde", 23 | "wikibase-rdf-entity-id-invalid": "L’identifiant d’entité que vous avez spécifié n’est pas valide", 24 | "wikibase-rdf-permission-denied": "Vous n’avez pas le droit de modifier les correspondances de cette entité", 25 | "wikibase-rdf-config-invalid": "Prédicat{{PLURAL:$1||s}} non valide{{PLURAL:$1||s}} :\n$2. Vos modifications n’ont pas été enregistrées.", 26 | "wikibase-rdf-config-list-intro": "Cette page définit quels prédicats peuvent être utilisés dans les correspondances vers d’autres ontologies. Cette fonctionnalité fait partie de [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF l’extension Wikibase RDF].", 27 | "wikibase-rdf-config-list-heading": "Prédicat{{PLURAL:$1||s}} autorisé{{PLURAL:$1||s}} :", 28 | "wikibase-rdf-config-list-empty": "Aucun prédicat n’est défini.", 29 | "wikibase-rdf-config-list-footer": "Défini dans « LocalSettings.php », ne peut pas être modifié via cette page.", 30 | "wikibase-rdf-property-edit-warning": "En cliquant sur « $1 », vous créez une déclaration permanente dans le stockage de triplets. Veuillez noter que toute modification ou suppression ultérieure des correspondances de propriétés nécessitera une réinitialisation du stockage de triplets afin d’effacer les correspondances obsolètes. Cela ne s’applique pas aux correspondances d'éléments.", 31 | "wikibase-rdf-property-edit-warning-acknowledge": "Ne plus afficher ce message", 32 | "special-mapping-predicates": "Prédicats de correspondance" 33 | } 34 | -------------------------------------------------------------------------------- /i18n/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Amire80" 5 | ] 6 | }, 7 | "wikibase-rdf-description": "אפשרות להגדיר מיפויי RDF לישויות ויקיבייס", 8 | "wikibase-rdf-mappings-toggler": "מיפוי לאונטולוגיות אחרות", 9 | "wikibase-rdf-mappings-predicate-heading": "פרדיקט", 10 | "wikibase-rdf-mappings-object-heading": "כתובת URL", 11 | "wikibase-rdf-mappings-action-save": "שמירה", 12 | "wikibase-rdf-mappings-action-remove": "הסרה", 13 | "wikibase-rdf-mappings-action-cancel": "ביטול", 14 | "wikibase-rdf-mappings-action-edit": "עריכה", 15 | "wikibase-rdf-mappings-action-add": "הוספת מיפוי", 16 | "wikibase-rdf-save-mappings-invalid-mappings": "חלק מהמיפויים שציינת אינם תקינים", 17 | "wikibase-rdf-save-mappings-save-failed": "השמירה נכשלה", 18 | "wikibase-rdf-entity-id-invalid": "מזהה הישות שנתת אינו תקין", 19 | "wikibase-rdf-permission-denied": "אין לך הרשאה לערוך את המיפויים של הישות הזאת", 20 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|פרדיקט בלתי־תקין|פרדיקטים בלתי־תקינים}}:\n$2. השינויים שלך לא נשמרו.", 21 | "wikibase-rdf-config-list-intro": "הדף הזה מגדיר באילו פרדיקטים ניתן להשתמש במיפויים לאונטולוגיות אחרות. הפונקציונליות הזאת היא חלק מההרחבה [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF].", 22 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|פרדיקט מותר|פרדיקטים מותרים}}:", 23 | "wikibase-rdf-config-list-empty": "לא מוגדרים פרדיקטים.", 24 | "wikibase-rdf-config-list-footer": "מוגדר ב־LocalSettings.php, לא ניתן לשנות דרך הדף הזה.", 25 | "wikibase-rdf-property-edit-warning": "לחיצה על \"$1\" תגרום ליצירה של קביעה קבועה באחסון שלישיות. לידיעתך, כל עריכה למאפיין אחרי־כן תדרוש איפוס של אחסון השלישיות כדי לנקות מיפויים מיושנים. זה לא חל על מיפוי פריטים", 26 | "wikibase-rdf-property-edit-warning-acknowledge": "לא להציג את ההודעה הזאת שוב.", 27 | "special-mapping-predicates": "מיפוי פרדיקטים" 28 | } 29 | -------------------------------------------------------------------------------- /i18n/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Abijeet Patro" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-predicate-heading": "सम्बन्ध", 8 | "wikibase-rdf-mappings-action-save": "सहेजें", 9 | "wikibase-rdf-mappings-action-remove": "हटायें", 10 | "wikibase-rdf-mappings-action-cancel": "रद्द करें", 11 | "wikibase-rdf-mappings-action-edit": "सम्पादन" 12 | } 13 | -------------------------------------------------------------------------------- /i18n/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hanna Tardos" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-predicate-heading": "Kapcsolat", 8 | "wikibase-rdf-mappings-object-heading": "URL", 9 | "wikibase-rdf-mappings-action-save": "mentés", 10 | "wikibase-rdf-mappings-action-remove": "eltávolítás", 11 | "wikibase-rdf-mappings-action-cancel": "mégsem", 12 | "wikibase-rdf-mappings-action-edit": "szerkesztés", 13 | "wikibase-rdf-save-mappings-save-failed": "Sikertelen mentés" 14 | } 15 | -------------------------------------------------------------------------------- /i18n/ia.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie" 5 | ] 6 | }, 7 | "wikibase-rdf-description": "Permitte definir mappamentos RDF pro entitates Wikibase", 8 | "wikibase-rdf-mappings-toggler": "Mappamento a altere ontologias", 9 | "wikibase-rdf-mappings-predicate-heading": "Predicato", 10 | "wikibase-rdf-mappings-object-heading": "URL", 11 | "wikibase-rdf-mappings-action-save": "salveguardar", 12 | "wikibase-rdf-mappings-action-remove": "remover", 13 | "wikibase-rdf-mappings-action-cancel": "cancellar", 14 | "wikibase-rdf-mappings-action-edit": "modificar", 15 | "wikibase-rdf-mappings-action-add": "adder mappamento", 16 | "wikibase-rdf-save-mappings-invalid-mappings": "Alcunes del mappamentos que tu ha specificate non es valide.", 17 | "wikibase-rdf-save-mappings-save-failed": "Salveguarda fallite", 18 | "wikibase-rdf-entity-id-invalid": "Le ID de entitate que tu ha specificate non es valide.", 19 | "wikibase-rdf-permission-denied": "Tu non ha le permission de modificar le mappamentos de iste entitate.", 20 | "wikibase-rdf-config-invalid": "Predicato{{PLURAL:$1||s}} non valide: $2. Tu cambiamentos non ha essite salveguardate.", 21 | "wikibase-rdf-config-list-intro": "Iste pagina defini qual predicatos pote esser usate in mappamentos a altere ontologias. Iste functionalitate face parte del [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF extension Wikibase RDF].", 22 | "wikibase-rdf-config-list-heading": "Predicato{{PLURAL:$1||s}} permittite:", 23 | "wikibase-rdf-config-list-empty": "Nulle predicato es definite.", 24 | "wikibase-rdf-config-list-footer": "Definite in LocalSettings.php, non pote esser modificate a transverso iste pagina.", 25 | "wikibase-rdf-property-edit-warning": "Cliccante sur \"$1\", tu crea un declaration permanente in le immagazinage de triplettos. Nota ben que omne modificationes subsequente del proprietate necessitara un reinitialisation del immagazinage de triplettos a fin de rader le mappamentos obsolete. Isto non se applica al mappamentos de elementos.", 26 | "wikibase-rdf-property-edit-warning-acknowledge": "Non monstrar iste message de novo.", 27 | "special-mapping-predicates": "Predicatos de mappamento" 28 | } 29 | -------------------------------------------------------------------------------- /i18n/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Rex", 5 | "Veracious" 6 | ] 7 | }, 8 | "wikibase-rdf-mappings-object-heading": "URL", 9 | "wikibase-rdf-mappings-action-save": "simpan", 10 | "wikibase-rdf-mappings-action-cancel": "batalkan", 11 | "wikibase-rdf-mappings-action-edit": "sunting", 12 | "wikibase-rdf-mappings-action-add": "tambahkan pemetaan", 13 | "wikibase-rdf-save-mappings-save-failed": "Gagal menyimpan", 14 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Predikat|Predikat-predikat}} tidak valid:\n$2. Perubahan Anda tidak tersimpan.", 15 | "special-mapping-predicates": "Predikat pemetaan" 16 | } 17 | -------------------------------------------------------------------------------- /i18n/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Beta16" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-object-heading": "URL", 8 | "wikibase-rdf-mappings-action-save": "salva", 9 | "wikibase-rdf-mappings-action-remove": "rimuovi", 10 | "wikibase-rdf-mappings-action-cancel": "annulla", 11 | "wikibase-rdf-mappings-action-edit": "modifica", 12 | "wikibase-rdf-property-edit-warning-acknowledge": "Non mostrare più questo messaggio." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "MathXplore", 5 | "Mugenpman", 6 | "Sou0012" 7 | ] 8 | }, 9 | "wikibase-rdf-mappings-predicate-heading": "述語", 10 | "wikibase-rdf-mappings-object-heading": "URL", 11 | "wikibase-rdf-mappings-action-save": "保存", 12 | "wikibase-rdf-mappings-action-remove": "除去", 13 | "wikibase-rdf-mappings-action-cancel": "キャンセル", 14 | "wikibase-rdf-mappings-action-edit": "編集", 15 | "wikibase-rdf-mappings-action-add": "追加マッピング", 16 | "wikibase-rdf-save-mappings-invalid-mappings": "指定されたマッピングの一部が無効です。", 17 | "wikibase-rdf-save-mappings-save-failed": "保存に失敗しました", 18 | "wikibase-rdf-entity-id-invalid": "指定されたエンティティIDが無効です", 19 | "wikibase-rdf-property-edit-warning-acknowledge": "今後このメッセージを表示しない" 20 | } 21 | -------------------------------------------------------------------------------- /i18n/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ykhwong" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-predicate-heading": "관계", 8 | "wikibase-rdf-mappings-object-heading": "URL", 9 | "wikibase-rdf-mappings-action-save": "저장", 10 | "wikibase-rdf-mappings-action-remove": "제거", 11 | "wikibase-rdf-mappings-action-cancel": "취소", 12 | "wikibase-rdf-mappings-action-edit": "편집", 13 | "wikibase-rdf-save-mappings-save-failed": "저장 실패", 14 | "wikibase-rdf-property-edit-warning-acknowledge": "이 메시지를 더 이상 보지 않습니다." 15 | } 16 | -------------------------------------------------------------------------------- /i18n/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Volvox" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-object-heading": "URL", 8 | "wikibase-rdf-mappings-action-save": "späicheren", 9 | "wikibase-rdf-mappings-action-remove": "ewechhuelen", 10 | "wikibase-rdf-mappings-action-cancel": "ofbriechen", 11 | "wikibase-rdf-mappings-action-edit": "änneren", 12 | "wikibase-rdf-property-edit-warning-acknowledge": "Dëse Message net méi weisen." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/lfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Elefentiste" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-predicate-heading": "Predica", 8 | "wikibase-rdf-mappings-object-heading": "URL", 9 | "wikibase-rdf-mappings-action-save": "fisa", 10 | "wikibase-rdf-mappings-action-remove": "sutrae", 11 | "wikibase-rdf-mappings-action-cancel": "cansela", 12 | "wikibase-rdf-mappings-action-edit": "edita" 13 | } 14 | -------------------------------------------------------------------------------- /i18n/lv.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Papuass" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-object-heading": "URL", 8 | "wikibase-rdf-mappings-action-save": "saglabāt", 9 | "wikibase-rdf-mappings-action-remove": "noņemt", 10 | "wikibase-rdf-mappings-action-cancel": "atcelt" 11 | } 12 | -------------------------------------------------------------------------------- /i18n/mk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Bjankuloski06", 5 | "Kghbln", 6 | "Vlad5250" 7 | ] 8 | }, 9 | "wikibase-rdf-description": "Овозможува определување на RDF-пресликувања за единици во Викибазата", 10 | "wikibase-rdf-mappings-toggler": "Пресликување во други онтологии", 11 | "wikibase-rdf-mappings-predicate-heading": "Исказ", 12 | "wikibase-rdf-mappings-object-heading": "URL", 13 | "wikibase-rdf-mappings-action-save": "зачувај", 14 | "wikibase-rdf-mappings-action-remove": "отстрани", 15 | "wikibase-rdf-mappings-action-cancel": "откажи", 16 | "wikibase-rdf-mappings-action-edit": "уреди", 17 | "wikibase-rdf-mappings-action-add": "додај пресликување", 18 | "wikibase-rdf-save-mappings-invalid-mappings": "Некои од укажаните пресликувања се неважечки", 19 | "wikibase-rdf-save-mappings-save-failed": "Зачувувањето не успеа", 20 | "wikibase-rdf-entity-id-invalid": "Укажаната назнака на единицата е неважечка", 21 | "wikibase-rdf-permission-denied": "Немате дозвола да ги менувате пресликувањата на оваа единица", 22 | "wikibase-rdf-config-invalid": "Неважечки {{PLURAL:$1|предикат|предикати}}:\n$2. Промените не се зачувани.", 23 | "wikibase-rdf-config-list-intro": "Страницава определува кои предикати ќе можат да се користат во пресликување во други онтологии. Оваа функција е дел од [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDFдодадокот Викибаза RDF].", 24 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Дозволен предикат|Дозволени предикати}}:", 25 | "wikibase-rdf-config-list-empty": "Нема определено предикати.", 26 | "wikibase-rdf-config-list-footer": "Определено во LocalSettings.php. Не може да се менуива преку оваа страница.", 27 | "wikibase-rdf-property-edit-warning": "Стискајќи на „$1“ создавате трајна изјава во складот на тројки. Имајте на ум дека секоја понатамошна измена на својството бара препуштање на складот за да се исчистат застарените пресликувања. Ова не важи за пресликувања на предмети", 28 | "wikibase-rdf-property-edit-warning-acknowledge": "Повеќе не ја покажувај поракава.", 29 | "special-mapping-predicates": "Предикати на пресликување" 30 | } 31 | -------------------------------------------------------------------------------- /i18n/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kghbln", 5 | "McDutchie", 6 | "Romaine" 7 | ] 8 | }, 9 | "wikibase-rdf-description": "Maakt het definiëren van RDF-toewijzingen voor Wikibase-entiteiten mogelijk", 10 | "wikibase-rdf-mappings-toggler": "Toewijzing aan andere ontologieën", 11 | "wikibase-rdf-mappings-predicate-heading": "Predikaat", 12 | "wikibase-rdf-mappings-object-heading": "URL", 13 | "wikibase-rdf-mappings-action-save": "opslaan", 14 | "wikibase-rdf-mappings-action-remove": "verwijderen", 15 | "wikibase-rdf-mappings-action-cancel": "annuleren", 16 | "wikibase-rdf-mappings-action-edit": "bewerken", 17 | "wikibase-rdf-mappings-action-add": "toewijzing toevoegen", 18 | "wikibase-rdf-save-mappings-invalid-mappings": "Sommige van de door {{#FORMAL:u|jou}} opgegeven toewijzingen zijn ongeldig", 19 | "wikibase-rdf-save-mappings-save-failed": "Opslaan mislukt", 20 | "wikibase-rdf-entity-id-invalid": "De entiteits-ID die {{#FORMAL:u|je}} hebt opgegeven, is ongeldig", 21 | "wikibase-rdf-permission-denied": "{{#FORMAL:U|Je}} hebt geen toestemming om de toewijzingen van deze entiteit te bewerken", 22 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Ongeldig predicaat|Ongeldige predicaten}}: $2. De wijzigingen zijn niet opgeslagen.", 23 | "wikibase-rdf-config-list-intro": "Deze pagina bepaalt welke predicaten kunnen worden gebruikt bij toewijzingen aan andere ontologieën. Deze functionaliteit maakt deel uit van de [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF RDF-uitbreiding voor Wikibase].", 24 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Toegestaan predicaat|Toegestane predicaten}}:", 25 | "wikibase-rdf-config-list-empty": "Er zijn geen predikaten gedefinieerd.", 26 | "wikibase-rdf-config-list-footer": "Gedefinieerd in LocalSettings.php, kan niet via deze pagina worden gewijzigd.", 27 | "wikibase-rdf-property-edit-warning": "Door op “$1” te klikken maakt u een permanent overzicht aan in de ''triplestore''. Houd er rekening mee dat voor eventuele volgende bewerkingen aan de eigenschap de ''triplestore'' opnieuw moet worden opgebouwd om verouderde toewijzingen te verwijderen. Dit geldt niet voor toewijzingen aan items.", 28 | "wikibase-rdf-property-edit-warning-acknowledge": "Dit bericht niet meer weergeven.", 29 | "special-mapping-predicates": "Toewijzingspredicaten" 30 | } 31 | -------------------------------------------------------------------------------- /i18n/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hamilton Abreu", 5 | "Kghbln" 6 | ] 7 | }, 8 | "wikibase-rdf-description": "Permite definir mapeamentos RDF para entidades Wikibase", 9 | "wikibase-rdf-mappings-toggler": "Mapeamento para outras ontologias", 10 | "wikibase-rdf-mappings-predicate-heading": "Predicado", 11 | "wikibase-rdf-mappings-object-heading": "URL", 12 | "wikibase-rdf-mappings-action-save": "gravar", 13 | "wikibase-rdf-mappings-action-remove": "remover", 14 | "wikibase-rdf-mappings-action-cancel": "cancelar", 15 | "wikibase-rdf-mappings-action-edit": "editar", 16 | "wikibase-rdf-mappings-action-add": "adicionar mapeamento", 17 | "wikibase-rdf-save-mappings-invalid-mappings": "Alguns dos mapeamentos que especificou são inválidos", 18 | "wikibase-rdf-save-mappings-save-failed": "A gravação falhou", 19 | "wikibase-rdf-entity-id-invalid": "O identificador da entidade que especificou é inválido", 20 | "wikibase-rdf-permission-denied": "Não tem permissão para editar os mapeamentos desta entidade", 21 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Predicado inválido|Predicados inválidos}}:\n$2. As suas alterações não foram gravadas.", 22 | "wikibase-rdf-config-list-intro": "Esta página define os predicados que podem ser usados em mapeamentos para outras ontologias. Esta funcionalidade faz parte da [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF extensão Wikibase RDF].", 23 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Predicado permitido|Predicados permitidos}}:", 24 | "wikibase-rdf-config-list-empty": "Não está definido nenhum predicado.", 25 | "wikibase-rdf-config-list-footer": "Definido em LocalSettings.php, não pode ser modificado através desta página.", 26 | "wikibase-rdf-property-edit-warning": "Ao clicar \"$1\" está a criar uma declaração permanente no repositório de triplas. Note que quaisquer edições subsequentes da propriedade exigirão uma reinicialização do repositório de triplas para limpar mapeamentos desatualizados. Isto não se aplica a mapeamentos de elementos", 27 | "wikibase-rdf-property-edit-warning-acknowledge": "Não voltar a mostrar esta mensagem.", 28 | "special-mapping-predicates": "Predicados de mapeamento" 29 | } 30 | -------------------------------------------------------------------------------- /i18n/qqq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Amire80", 5 | "Jeroen De Dauw", 6 | "Kghbln" 7 | ] 8 | }, 9 | "wikibase-rdf-name": "{{notranslate}}", 10 | "wikibase-rdf-description": "{{Desc|name=Wikibase RDF|url=https://github.com/ProfessionalWiki/WikibaseRDF}}", 11 | "wikibase-rdf-mappings-toggler": "This is the text on a button.", 12 | "wikibase-rdf-mappings-predicate-heading": "This is a section header.", 13 | "wikibase-rdf-mappings-object-heading": "This is a section header.", 14 | "wikibase-rdf-mappings-action-save": "This is the text on a button.", 15 | "wikibase-rdf-mappings-action-remove": "This is the text on a button.", 16 | "wikibase-rdf-mappings-action-cancel": "This is the text on a button.", 17 | "wikibase-rdf-mappings-action-edit": "This is the text on a button.", 18 | "wikibase-rdf-mappings-action-add": "This is the text on a button.", 19 | "wikibase-rdf-save-mappings-invalid-mappings": "This is an error message.", 20 | "wikibase-rdf-save-mappings-save-failed": "This is an error message.", 21 | "wikibase-rdf-entity-id-invalid": "This is an error message.", 22 | "wikibase-rdf-permission-denied": "This is an error message. Mappings as in RDF mappings.", 23 | "wikibase-rdf-config-intro": "This is the text shown at the top of the form on \"MediaWiki:MappingPredicates\" when editing.", 24 | "wikibase-rdf-config-invalid": "Error message shown when attempting to save invalid configuration on \"MediaWiki:MappingPredicates\"", 25 | "wikibase-rdf-config-list-intro": "This is the text shown at the top of \"MediaWiki:MappingPredicates\" when viewing.", 26 | "wikibase-rdf-config-list-heading": "This is the text shown before the list of predicates on \"MediaWiki:MappingPredicates\".", 27 | "wikibase-rdf-config-list-empty": "This is the text shown on \"MediaWiki:MappingPredicates\" when no predicates are defined.", 28 | "wikibase-rdf-config-list-footer": "This is the text shown at the bottom of \"MediaWiki:MappingPredicates\" when viewing.", 29 | "wikibase-rdf-property-edit-warning": "This is the text of the popup shown when editing a property mapping", 30 | "wikibase-rdf-property-edit-warning-version": "{{notranslate}}\nThe version of the property edit warning message. Used to indicate if a new message should be shown.", 31 | "wikibase-rdf-property-edit-warning-acknowledge": "This is the text of the action to acknowledge the property edit warning", 32 | "special-mapping-predicates": "This is the label on \"Special:SpecialPages\" linking to \"MediaWiki:MappingPredicates\"." 33 | } 34 | -------------------------------------------------------------------------------- /i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kareyac", 5 | "Pacha Tchernof", 6 | "Yurina Tatiana" 7 | ] 8 | }, 9 | "wikibase-rdf-mappings-object-heading": "URL", 10 | "wikibase-rdf-mappings-action-save": "сохранить", 11 | "wikibase-rdf-mappings-action-remove": "удалить", 12 | "wikibase-rdf-mappings-action-cancel": "отменить", 13 | "wikibase-rdf-mappings-action-edit": "править", 14 | "wikibase-rdf-save-mappings-save-failed": "Не удалось сохранить", 15 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Недопустимый предикат|Недопустимые предикаты}}:\n$2. Ваши изменения не были сохранены." 16 | } 17 | -------------------------------------------------------------------------------- /i18n/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eleassar", 5 | "Kghbln" 6 | ] 7 | }, 8 | "wikibase-rdf-description": "Omogoča opredelitev preslikav RDF za entitete Wikibase", 9 | "wikibase-rdf-mappings-toggler": "Preslikava v druge ontologije", 10 | "wikibase-rdf-mappings-predicate-heading": "Povedek", 11 | "wikibase-rdf-mappings-object-heading": "URL", 12 | "wikibase-rdf-mappings-action-save": "shrani", 13 | "wikibase-rdf-mappings-action-remove": "odstrani", 14 | "wikibase-rdf-mappings-action-cancel": "prekliči", 15 | "wikibase-rdf-mappings-action-edit": "uredi", 16 | "wikibase-rdf-mappings-action-add": "dodaj preslikavo", 17 | "wikibase-rdf-save-mappings-invalid-mappings": "Nekatere preslikave, ki ste jih določili, so neveljavne", 18 | "wikibase-rdf-save-mappings-save-failed": "Shranjevanje ni uspelo", 19 | "wikibase-rdf-entity-id-invalid": "ID entitete, ki ste ga navedli, je neveljaven", 20 | "wikibase-rdf-permission-denied": "Nimate dovoljenja za urejanje preslikav te entitete", 21 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Neveljaven predikat|Neveljavna predikata|Neveljavni predikati}}:\n$2. Vaše spremembe niso bile shranjene.", 22 | "wikibase-rdf-config-list-intro": "Ta stran določa, katere predikate je mogoče uporabiti pri preslikavah v druge ontologije. Ta funkcionalnost je del [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF razširitve Wikibase RDF].", 23 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Dovoljeni predikat|Dovoljena predikata|Dovoljeni predikati}}:", 24 | "wikibase-rdf-config-list-empty": "Opredeljen ni noben predikat.", 25 | "wikibase-rdf-config-list-footer": "Določeno v LocalSettings.php; na tej strani ni mogoče spreminjati.", 26 | "wikibase-rdf-property-edit-warning": "S klikom »$1« ustvarite v triplestore trajno izjavo. Upoštevajte, da bodo vsa poznejša urejanja lastnosti zahtevala ponastavitev triplestore, da se počistijo zastarele preslikave. To ne velja za preslikave predmetov.", 27 | "wikibase-rdf-property-edit-warning-acknowledge": "Tega sporočila ne prikaži več.", 28 | "special-mapping-predicates": "Predikati preslikave" 29 | } 30 | -------------------------------------------------------------------------------- /i18n/sms.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yupik" 5 | ] 6 | }, 7 | "wikibase-rdf-mappings-object-heading": "URL", 8 | "wikibase-rdf-mappings-action-save": "ruõkk", 9 | "wikibase-rdf-mappings-action-remove": "jaukkâd", 10 | "wikibase-rdf-mappings-action-cancel": "jõõsk", 11 | "wikibase-rdf-mappings-action-edit": "muuʹtt", 12 | "wikibase-rdf-save-mappings-save-failed": "Ruõkkmõš ij oʹnnstam" 13 | } 14 | -------------------------------------------------------------------------------- /i18n/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kghbln", 5 | "Sabelöga" 6 | ] 7 | }, 8 | "wikibase-rdf-description": "Wikibase-tillägg som gör det möjligt att definiera RDF-kartläggningar för Wikibase-entiteter", 9 | "wikibase-rdf-mappings-toggler": "Kartläggning till andra ontologier", 10 | "wikibase-rdf-mappings-predicate-heading": "Predikat", 11 | "wikibase-rdf-mappings-object-heading": "URL", 12 | "wikibase-rdf-mappings-action-save": "spara", 13 | "wikibase-rdf-mappings-action-remove": "ta bort", 14 | "wikibase-rdf-mappings-action-cancel": "avbryt", 15 | "wikibase-rdf-mappings-action-edit": "redigera", 16 | "wikibase-rdf-mappings-action-add": "spara kartläggning", 17 | "wikibase-rdf-save-mappings-invalid-mappings": "Vissa kartläggningar du angett är ogiltiga", 18 | "wikibase-rdf-save-mappings-save-failed": "Kunde inte spara", 19 | "wikibase-rdf-entity-id-invalid": "Entitets-ID:t du angett är ogiltigt", 20 | "wikibase-rdf-permission-denied": "Du har inte behörighet att redigera detta entitets kartläggningar", 21 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|Ogiltigt predikat|Ogiltiga predikat}}:\n$2. Dina ändringar sparades inte.", 22 | "wikibase-rdf-config-list-intro": "Denna sida definierar vilka predikat som kan användas i kartläggningar till andra ontologier. Denna funktion är en del av [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF tillägget Wikibase RDF].", 23 | "wikibase-rdf-config-list-heading": "{{PLURAL:$1|Tillåtet predikat|Tillåtna predikat}}:", 24 | "wikibase-rdf-config-list-empty": "Inga predikat har definierats.", 25 | "wikibase-rdf-config-list-footer": "Definierats i LocalSettings.php, kan inte ändras via den här sidan.", 26 | "wikibase-rdf-property-edit-warning-acknowledge": "Visa inte det här meddelandet igen.", 27 | "special-mapping-predicates": "Kartläggningspredikat" 28 | } 29 | -------------------------------------------------------------------------------- /i18n/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hedda", 5 | "Rapsar" 6 | ] 7 | }, 8 | "wikibase-rdf-description": "Wikibase Varlıkları için RDF eşlemelerinin tanımlanmasına izin veren Wikibase uzantısı", 9 | "wikibase-rdf-mappings-predicate-heading": "İlişki", 10 | "wikibase-rdf-mappings-object-heading": "URL", 11 | "wikibase-rdf-mappings-action-save": "kaydet", 12 | "wikibase-rdf-mappings-action-remove": "kaldır", 13 | "wikibase-rdf-mappings-action-cancel": "iptal", 14 | "wikibase-rdf-mappings-action-edit": "değiştir", 15 | "wikibase-rdf-save-mappings-save-failed": "kayıt başarısız", 16 | "wikibase-rdf-entity-id-invalid": "Belirttiğiniz varlık kimliği geçersiz" 17 | } 18 | -------------------------------------------------------------------------------- /i18n/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Chimes", 5 | "Corn Pig", 6 | "GuoPC", 7 | "Lakejason0", 8 | "MangoFanFanw", 9 | "沈澄心" 10 | ] 11 | }, 12 | "wikibase-rdf-description": "允许为Wikibase实体定义RDF映射", 13 | "wikibase-rdf-mappings-toggler": "映射到其他本体", 14 | "wikibase-rdf-mappings-predicate-heading": "谓词", 15 | "wikibase-rdf-mappings-object-heading": "URL", 16 | "wikibase-rdf-mappings-action-save": "保存", 17 | "wikibase-rdf-mappings-action-remove": "移除", 18 | "wikibase-rdf-mappings-action-cancel": "取消", 19 | "wikibase-rdf-mappings-action-edit": "编辑", 20 | "wikibase-rdf-mappings-action-add": "添加映射", 21 | "wikibase-rdf-save-mappings-invalid-mappings": "您指定的某些映射无效", 22 | "wikibase-rdf-save-mappings-save-failed": "保存失败", 23 | "wikibase-rdf-entity-id-invalid": "您指定的实体ID无效", 24 | "wikibase-rdf-permission-denied": "您无权编辑此实体的映射", 25 | "wikibase-rdf-config-invalid": "无效{{PLURAL:$1|predicate|predicates}}:$2 。您的更改未被保存。", 26 | "wikibase-rdf-config-list-intro": "本页定义了哪些谓词可用于映射到其他本体。此功能是[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF扩展]的一部分。", 27 | "wikibase-rdf-config-list-heading": "允许{{{{PLURAL:$1|predicate|predicates}}}}:", 28 | "wikibase-rdf-config-list-empty": "没有定义谓词。", 29 | "wikibase-rdf-config-list-footer": "已在LocalSettings.php中定义,无法通过此页面修改。", 30 | "wikibase-rdf-property-edit-warning": "透過點擊「$1」,您會在三元組儲存中建立永久聲明。請注意,對該屬性做出的任何後續編輯都需要重新設定三元組儲存,以清除過時的對映。另外這不適用於項目對映", 31 | "wikibase-rdf-property-edit-warning-acknowledge": "不再显示此消息。", 32 | "special-mapping-predicates": "映射谓词" 33 | } 34 | -------------------------------------------------------------------------------- /i18n/zh-hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "August.C", 5 | "Kghbln", 6 | "Kly", 7 | "Winston Sung" 8 | ] 9 | }, 10 | "wikibase-rdf-description": "允許為 Wikibase 實體定義 RDF 對映", 11 | "wikibase-rdf-mappings-toggler": "對映到其他本體", 12 | "wikibase-rdf-mappings-predicate-heading": "述詞", 13 | "wikibase-rdf-mappings-object-heading": "URL", 14 | "wikibase-rdf-mappings-action-save": "儲存", 15 | "wikibase-rdf-mappings-action-remove": "移除", 16 | "wikibase-rdf-mappings-action-cancel": "取消", 17 | "wikibase-rdf-mappings-action-edit": "編輯", 18 | "wikibase-rdf-mappings-action-add": "新增對映", 19 | "wikibase-rdf-save-mappings-invalid-mappings": "您指定的某些對映無效", 20 | "wikibase-rdf-save-mappings-save-failed": "儲存失敗", 21 | "wikibase-rdf-entity-id-invalid": "您指定的實體 ID 無效", 22 | "wikibase-rdf-permission-denied": "您沒有編輯該實體對映的權限", 23 | "wikibase-rdf-config-invalid": "{{PLURAL:$1|述詞}}無效:$2。未儲存您的更改。", 24 | "wikibase-rdf-config-list-intro": "此頁面定義了哪些謂詞可用於對應至其他本體 (ontologies)。此功能是 [https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Wikibase_RDF Wikibase RDF 擴充功能] 的一部分。", 25 | "wikibase-rdf-config-list-heading": "允許{{PLURAL:$1|述詞}}:", 26 | "wikibase-rdf-config-list-empty": "沒有定義述詞。", 27 | "wikibase-rdf-config-list-footer": "在 LocalSettings.php 中定義,無法透過此頁面變動。", 28 | "wikibase-rdf-property-edit-warning": "透過點擊「$1」,您會在三元組儲存中建立永久聲明。請注意,對該屬性做出的任何後續編輯都需要重新設定三元組儲存,以清除過時的對映。另外這不適用於項目對映", 29 | "wikibase-rdf-property-edit-warning-acknowledge": "不要再顯示此訊息。", 30 | "special-mapping-predicates": "對映述詞" 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "grunt test" 5 | }, 6 | "devDependencies": { 7 | "api-testing": "^1.4.2", 8 | "eslint-config-wikimedia": "0.20.0", 9 | "grunt": "1.4.0", 10 | "grunt-banana-checker": "0.9.0", 11 | "grunt-eslint": "23.0.0", 12 | "grunt-stylelint": "0.16.0", 13 | "stylelint-config-wikimedia": "0.11.1" 14 | }, 15 | "eslintIgnore": [ 16 | "coverage", 17 | "vendor" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | src/ 4 | tests/ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Constant WB_NS_ITEM not found\\.$#" 5 | count: 1 6 | path: src/EntryPoints/MediaWikiHooks.php 7 | 8 | - 9 | message: "#^Constant WB_NS_PROPERTY not found\\.$#" 10 | count: 1 11 | path: src/EntryPoints/MediaWikiHooks.php 12 | 13 | - 14 | message: "#^Cannot access offset 'PT\\:url' on mixed\\.$#" 15 | count: 1 16 | path: src/WikibaseRdfExtension.php 17 | 18 | - 19 | message: "#^Cannot cast mixed to int\\.$#" 20 | count: 1 21 | path: src/WikibaseRdfExtension.php 22 | 23 | - 24 | message: "#^Parameter \\#1 \\$database of class ProfessionalWiki\\\\WikibaseRDF\\\\Persistence\\\\SqlAllMappingsLookup constructor expects Wikimedia\\\\Rdbms\\\\IDatabase, Wikimedia\\\\Rdbms\\\\IDatabase\\|false given\\.$#" 25 | count: 1 26 | path: src/WikibaseRdfExtension.php 27 | 28 | - 29 | message: "#^Parameter \\#4 \\$availableRights of class Wikibase\\\\Repo\\\\Store\\\\WikiPageEntityStorePermissionChecker constructor expects array\\, array given\\.$#" 30 | count: 1 31 | path: src/WikibaseRdfExtension.php 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 9 6 | paths: 7 | - src 8 | scanDirectories: 9 | - ../../includes 10 | - ../../tests/phpunit 11 | - ../../vendor 12 | - ../../extensions/Wikibase 13 | bootstrapFiles: 14 | - ../../includes/AutoLoader.php 15 | # excludePaths: 16 | # analyse: 17 | # - src/AutomatedValuesFactory.php 18 | # ignoreErrors: 19 | # - '#.+ no value type specified in iterable type .+#' 20 | # - '#Constant [a-zA-Z0-9\\_]+ not found#' 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | 9 | src 10 | 11 | 12 | 13 | 14 | Performance 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $mappings 6 | 7 | 8 | $body 9 | $mappings 10 | 11 | 12 | 13 | 14 | $array 15 | 16 | 17 | $array 18 | 19 | 20 | 21 | 22 | join 23 | join 24 | join 25 | 26 | 27 | 28 | 29 | (array)$services->getMainConfig()->get( 'AvailableRights' ) 30 | 31 | 32 | WikibaseRepo::getSettings()->getSetting( 'string-limits' )['PT:url'] 33 | 34 | 35 | MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( ILoadBalancer::DB_REPLICA ) 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /resources/ext.wikibase.rdf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript for WikibaseRDF 3 | */ 4 | 5 | $( function () { 6 | 'use strict'; 7 | 8 | function moveSection() { 9 | // Move Mappings before Statements section. 10 | $( '.wikibase-rdf' ).insertBefore( $( 'h2.wikibase-statements' ) ).show(); 11 | } 12 | 13 | function addToggler() { 14 | // TODO: toggler state needs to be remembered. 15 | const toggler = $( '.wikibase-rdf-toggler' ).toggler( { 16 | $subject: $( '.wikibase-rdf-mappings' ), 17 | visible: false 18 | } ); 19 | toggler.find( '.ui-toggler-label' ).text( mw.msg( 'wikibase-rdf-mappings-toggler' ) ); 20 | } 21 | 22 | function findRow( element ) { 23 | return $( element ).parents( '.wikibase-rdf-row' ); 24 | } 25 | 26 | function showPropertyEditWarning( $anchor ) { 27 | if ( mw.config.get( 'wgCanonicalNamespace' ) !== 'Property' ) { 28 | return; 29 | } 30 | 31 | const version = mw.msg( 'wikibase-rdf-property-edit-warning-version' ), 32 | cookieKey = 'wikibase-rdf.acknowledge-property-edit', 33 | optionsKey = 'wikibase-rdf-acknowledge-property-edit'; 34 | 35 | if ( mw.cookie.get( cookieKey ) === version || 36 | mw.user.options.get( optionsKey ) === version 37 | ) { 38 | return; 39 | } 40 | 41 | const messageText = mw.msg( 'wikibase-rdf-property-edit-warning', mw.msg( 'wikibase-rdf-mappings-action-save' ) ); 42 | 43 | const $message = $( '

' + messageText + '

' ) 44 | .addClass( 'wikibase-rdf-property-edit-warning-container' ), 45 | $hideMessage = $( '' ) 46 | .text( mw.msg( 'wikibase-rdf-property-edit-warning-acknowledge' ) ) 47 | .appendTo( $message ); 48 | 49 | const $messageAnchor = $( '' ) 50 | .appendTo( document.body ) 51 | .toolbaritem() 52 | .wbtooltip( { 53 | content: $message, 54 | permanent: true, 55 | gravity: 'sw', 56 | $anchor: $anchor 57 | } ); 58 | 59 | $hideMessage.on( 'click', function ( event ) { 60 | event.preventDefault(); 61 | $messageAnchor.data( 'wbtooltip' ).degrade( true ); 62 | $( window ).off( '.wikibase-rdf-property-warning' ); 63 | if ( mw.user.isAnon() ) { 64 | mw.cookie.set( cookieKey, version, { expires: 3 * 365 * 24 * 60 * 60, path: '/' } ); 65 | } else { 66 | const api = new mw.Api(); 67 | api.saveOption( optionsKey, version ); 68 | } 69 | } ); 70 | 71 | $messageAnchor.data( 'wbtooltip' ).show(); 72 | 73 | $( window ).one( 74 | 'scroll.wikibase-rdf-property-warning touchmove.wikibase-rdf-property-warning resize.wikibase-rdf-property-warning', 75 | function () { 76 | const tooltip = $messageAnchor.data( 'wbtooltip' ); 77 | if ( tooltip ) { 78 | $messageAnchor.data( 'wbtooltip' ).hide(); 79 | } 80 | $( '.wikibase-entityview' ).off( '.wikibase-rdf-property-warning' ); 81 | } 82 | ); 83 | } 84 | 85 | function clickAdd( event ) { 86 | event.preventDefault(); 87 | 88 | const $row = $( '.wikibase-rdf-row-editing-template' ).clone(); 89 | $row.find( '.wikibase-rdf-action-remove' ).remove(); 90 | $row.removeClass( 'wikibase-rdf-row-editing-template' ); 91 | $row.addClass( 'wikibase-rdf-row' ); 92 | $row.addClass( 'wikibase-rdf-row-editing-add' ); 93 | $row.appendTo( $( '.wikibase-rdf-rows' ) ); 94 | 95 | showPropertyEditWarning( $row.find( '.wikibase-rdf-action-save' ) ); 96 | } 97 | 98 | function clickEdit( event ) { 99 | event.preventDefault(); 100 | 101 | const $row = findRow( event.target ); 102 | $row.html( $( '.wikibase-rdf-row-editing-template' ).html() ); 103 | $row.addClass( 'wikibase-rdf-row-editing-existing' ); 104 | $row.find( '[name="wikibase-rdf-predicate"]' ).val( $row.data( 'predicate' ) ).trigger( 'change' ); 105 | $row.find( '[name="wikibase-rdf-object"]' ).val( $row.data( 'object' ) ).trigger( 'change' ); 106 | 107 | showPropertyEditWarning( $row.find( '.wikibase-rdf-action-save' ) ); 108 | } 109 | 110 | function hideError() { 111 | $( '.wikibase-rdf-error' ).hide(); 112 | } 113 | 114 | function showError( error ) { 115 | $( '.wikibase-rdf-error' ).text( error ).show(); 116 | } 117 | 118 | function createObjectLink( url ) { 119 | const link = document.createElement( 'a' ); 120 | link.href = url; 121 | link.text = url; 122 | link.classList.add( 'external' ); 123 | return $( link ); 124 | } 125 | 126 | function onSuccessfulSave( trigger ) { 127 | const $row = findRow( trigger ); 128 | $row.data( 'predicate', $row.find( '[name="wikibase-rdf-predicate"]' ).val() ); 129 | $row.data( 'object', $row.find( '[name="wikibase-rdf-object"]' ).val() ); 130 | 131 | $row.removeClass( 'wikibase-rdf-row-editing-existing' ); 132 | $row.removeClass( 'wikibase-rdf-row-editing-add' ); 133 | 134 | $row.html( $( '.wikibase-rdf-row-template' ).html() ); 135 | $row.find( '.wikibase-rdf-predicate' ).text( $row.data( 'predicate' ) ); 136 | $row.find( '.wikibase-rdf-object' ).append( createObjectLink( $row.data( 'object' ) ) ); 137 | } 138 | 139 | function onSuccessfulRemove( trigger ) { 140 | findRow( trigger ).remove(); 141 | } 142 | 143 | function disableActions() { 144 | $( '.wikibase-rdf' ).addClass( 'wikibase-rdf-disabled' ); 145 | } 146 | 147 | function enableActions() { 148 | $( '.wikibase-rdf' ).removeClass( 'wikibase-rdf-disabled' ); 149 | } 150 | 151 | function saveMappings( trigger ) { 152 | disableActions(); 153 | 154 | const mappings = []; 155 | 156 | findRow( trigger ).addClass( 'wikibase-rdf-row-editing-saving' ); 157 | 158 | $( '.wikibase-rdf-rows .wikibase-rdf-row' ).each( function ( index, row ) { 159 | const mapping = { predicate: '', object: '' }; 160 | 161 | const $row = $( row ); 162 | const isAdd = $row.hasClass( 'wikibase-rdf-row-editing-add' ); 163 | const isEdit = $row.hasClass( 'wikibase-rdf-row-editing-existing' ); 164 | const isTrigger = $row.hasClass( 'wikibase-rdf-row-editing-saving' ); 165 | const isRemove = $( trigger ).hasClass( 'wikibase-rdf-action-remove' ); 166 | 167 | if ( isTrigger && isRemove ) { 168 | return; 169 | } else if ( isAdd || isEdit ) { 170 | if ( !isTrigger ) { 171 | return; 172 | } 173 | // Rows in edit mode should not be saved, unless it was the triggering row. 174 | mapping.predicate = $row.find( '[name="wikibase-rdf-predicate"]' ).val(); 175 | mapping.object = $row.find( '[name="wikibase-rdf-object"]' ).val(); 176 | $row.removeClass( 'wikibase-rdf-row-editing-saving' ); 177 | } else { 178 | mapping.predicate = $row.data( 'predicate' ); 179 | mapping.object = $row.data( 'object' ); 180 | } 181 | mappings.push( mapping ); 182 | } ); 183 | 184 | const api = new mw.Rest(); 185 | api.post( 186 | '/wikibase-rdf/v1/mappings/' + mw.config.get( 'wgTitle' ), 187 | { mappings: mappings }, 188 | { authorization: 'token' } 189 | ) 190 | .done( function () { 191 | hideError(); 192 | const isSave = $( trigger ).hasClass( 'wikibase-rdf-action-save' ); 193 | const isRemove = $( trigger ).hasClass( 'wikibase-rdf-action-remove' ); 194 | if ( isSave ) { 195 | onSuccessfulSave( trigger ); 196 | } else if ( isRemove ) { 197 | onSuccessfulRemove( trigger ); 198 | } 199 | enableActions(); 200 | } ) 201 | .fail( function ( data, response ) { 202 | const userLang = mw.config.get( 'wgUserLanguage' ); 203 | const siteLang = mw.config.get( 'wgContentLanguage' ); 204 | if ( Object.prototype.hasOwnProperty.call( response.xhr.responseJSON, 'messageTranslations' ) ) { 205 | if ( userLang in response.xhr.responseJSON.messageTranslations ) { 206 | showError( response.xhr.responseJSON.messageTranslations[ userLang ] ); 207 | } else if ( siteLang in response.xhr.responseJSON.messageTranslations ) { 208 | showError( response.xhr.responseJSON.messageTranslations[ siteLang ] ); 209 | } else { 210 | showError( response.xhr.responseJSON.messageTranslations.en ); 211 | } 212 | } else { 213 | // TOOD: Handle other message structures 214 | showError( JSON.stringify( response.xhr.responseJSON ) ); 215 | } 216 | enableActions(); 217 | } ); 218 | } 219 | 220 | function clickSave( event ) { 221 | event.preventDefault(); 222 | saveMappings( event.target ); 223 | } 224 | 225 | function clickRemove( event ) { 226 | event.preventDefault(); 227 | saveMappings( event.target ); 228 | } 229 | 230 | function clickCancel( event ) { 231 | event.preventDefault(); 232 | 233 | const $row = findRow( event.target ); 234 | 235 | if ( $row.hasClass( 'wikibase-rdf-row-editing-add' ) ) { 236 | $row.remove(); 237 | hideError(); 238 | return; 239 | } 240 | 241 | $row.html( $( '.wikibase-rdf-row-template' ).html() ); 242 | $row.removeClass( 'wikibase-rdf-row-editing' ); 243 | $row.find( '.wikibase-rdf-predicate' ).text( $row.data( 'predicate' ) ); 244 | $row.find( '.wikibase-rdf-object' ).text( $row.data( 'object' ) ); 245 | hideError(); 246 | } 247 | 248 | function setupEvents() { 249 | $( '.wikibase-rdf-action-add' ).on( 'click', clickAdd ); 250 | $( '.wikibase-rdf' ) 251 | .on( 'click', '.wikibase-rdf-action-edit', clickEdit ) 252 | .on( 'click', '.wikibase-rdf-action-save', clickSave ) 253 | .on( 'click', '.wikibase-rdf-action-remove', clickRemove ) 254 | .on( 'click', '.wikibase-rdf-action-cancel', clickCancel ); 255 | } 256 | 257 | function init() { 258 | moveSection(); 259 | mw.loader.using( 'wikibase.view.ControllerViewFactory', addToggler ); 260 | setupEvents(); 261 | } 262 | 263 | init(); 264 | 265 | } ); 266 | -------------------------------------------------------------------------------- /resources/ext.wikibase.rdf.less: -------------------------------------------------------------------------------- 1 | .wikibase-rdf { 2 | float: left; 3 | width: 100%; 4 | } 5 | 6 | .wikibase-rdf-mappings table { 7 | background: #fff; 8 | border: 1px solid #c8ccd1; 9 | border-spacing: 0; 10 | display: table; 11 | table-layout: fixed; 12 | width: 100%; 13 | word-wrap: break-word; 14 | } 15 | 16 | .wikibase-rdf-header th { 17 | background-color: #eaecf0; 18 | border-right: 1px solid #fff; 19 | padding: 0 0 0 0.4em; 20 | position: sticky; 21 | top: 0; 22 | font-weight: inherit; 23 | text-align: left; 24 | } 25 | 26 | .wikibase-rdf-mappings-predicate-heading { 27 | width: 15em; 28 | } 29 | 30 | .wikibase-rdf-mappings-actions-heading { 31 | width: 18em; 32 | } 33 | 34 | .wikibase-rdf-rows td { 35 | line-height: 136%; 36 | overflow: hidden; 37 | padding: 0.4em; 38 | text-overflow: ellipsis; 39 | vertical-align: top; 40 | } 41 | 42 | .wikibase-rdf-error { 43 | color: #d33; 44 | } 45 | 46 | .wikibase-rdf-disabled a { 47 | pointer-events: none; 48 | color: unset; 49 | } 50 | 51 | .wikibase-rdf-row-editing-template { 52 | display: none; 53 | } 54 | 55 | .wikibase-rdf-row-template { 56 | display: none; 57 | } 58 | 59 | .wikibase-rdf-predicate select { 60 | width: 100%; 61 | } 62 | 63 | .wikibase-rdf-object input { 64 | width: 100%; 65 | } 66 | 67 | .wikibase-rdf-actions a { 68 | font-size: small; 69 | } 70 | 71 | .wikibase-rdf .icon { 72 | background-position: center; 73 | background-repeat: no-repeat; 74 | background-size: 16px 16px; 75 | display: inline-block; 76 | width: 20px; 77 | height: 20px; 78 | vertical-align: middle; 79 | opacity: 0.87; 80 | } 81 | 82 | // TODO: check if these urls work if the wiki is on a subpath 83 | .wikibase-rdf-action-edit .icon { 84 | background-image: url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/edit.png ); 85 | /* @embed */ 86 | background-image: linear-gradient( transparent, transparent ), url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/edit.svg ); 87 | } 88 | 89 | .wikibase-rdf-action-save .icon { 90 | background-image: url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/check.png ); 91 | /* @embed */ 92 | background-image: linear-gradient( transparent, transparent ), url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/check.svg ); 93 | } 94 | 95 | .wikibase-rdf-action-remove .icon { 96 | background-image: url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/trash.png ); 97 | /* @embed */ 98 | background-image: linear-gradient( transparent, transparent ), url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/trash.svg ); 99 | } 100 | 101 | .wikibase-rdf-action-cancel .icon { 102 | background-image: url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/close.png ); 103 | /* @embed */ 104 | background-image: linear-gradient( transparent, transparent ), url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/close.svg ); 105 | } 106 | 107 | .wikibase-rdf-action-add .icon { 108 | background-image: url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/add.png ); 109 | /* @embed */ 110 | background-image: linear-gradient( transparent, transparent ), url( ../../Wikibase/view/resources/jquery/wikibase/toolbar/themes/default/images/icons/ooui/add.svg ); 111 | } 112 | 113 | .wikibase-rdf-footer { 114 | overflow: hidden; 115 | } 116 | 117 | .wikibase-rdf-footer-actions { 118 | float: right; 119 | padding: 0.4em; 120 | width: 18em; 121 | } 122 | 123 | .wikibase-rdf-action-add { 124 | font-size: small; 125 | } 126 | -------------------------------------------------------------------------------- /src/Application/AllMappingsLookup.php: -------------------------------------------------------------------------------- 1 | predicate, ':' ) ) { 16 | throw new InvalidArgumentException( 'Invalid predicate' ); 17 | } 18 | 19 | if ( !str_contains( $this->object, '://' ) ) { 20 | throw new InvalidArgumentException( 'Invalid object' ); 21 | } 22 | } 23 | 24 | public function getPredicateBase(): string { 25 | return explode( ':', $this->predicate, 2 )[0]; 26 | } 27 | 28 | public function getPredicateLocal(): string { 29 | return explode( ':', $this->predicate, 2 )[1]; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/MappingList.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $mappings = []; 13 | 14 | /** 15 | * @param array $mappings 16 | */ 17 | public function __construct( array $mappings = [] ) { 18 | array_walk( $mappings, [ $this, 'add' ] ); // PHP 8.1: use first class callable 19 | } 20 | 21 | private function add( Mapping $mapping ): void { 22 | $this->mappings[] = $mapping; 23 | } 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function asArray(): array { 29 | return $this->mappings; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/MappingListAndId.php: -------------------------------------------------------------------------------- 1 | predicate, ':' ) ) { 15 | throw new InvalidArgumentException( 'Invalid predicate' ); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Application/PredicateList.php: -------------------------------------------------------------------------------- 1 | predicates ) ) { 25 | $this->predicates[] = $predicate; 26 | } 27 | } 28 | 29 | public function plus( self $predicates ): self { 30 | return new self( array_merge( $this->predicates, $predicates->predicates ) ); 31 | } 32 | 33 | public function contains( string $predicate ): bool { 34 | return in_array( new Predicate( $predicate ), $this->predicates ); 35 | } 36 | 37 | /** 38 | * @return Predicate[] 39 | */ 40 | public function asArray(): array { 41 | return $this->predicates; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Application/SaveMappings/SaveMappingsPresenter.php: -------------------------------------------------------------------------------- 1 | $mappings 13 | */ 14 | public function presentInvalidMappings( array $mappings ): void; 15 | 16 | public function presentSaveFailed(): void; 17 | 18 | public function presentInvalidEntityId(): void; 19 | 20 | public function presentPermissionDenied(): void; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/SaveMappings/SaveMappingsUseCase.php: -------------------------------------------------------------------------------- 1 | $mappingsRequest 34 | */ 35 | public function saveMappings( string $entityIdValue, array $mappingsRequest ): void { 36 | try { 37 | $entityId = $this->entityIdParser->parse( $entityIdValue ); 38 | } catch ( EntityIdParsingException ) { 39 | $this->presenter->presentInvalidEntityId(); 40 | return; 41 | } 42 | 43 | if ( !$this->authorizer->canEditEntityMappings( $entityId ) ) { 44 | $this->presenter->presentPermissionDenied(); 45 | return; 46 | } 47 | 48 | if ( !$this->mappingsIsList( $mappingsRequest ) ) { 49 | $this->presenter->presentInvalidMappings( [] ); 50 | return; 51 | } 52 | 53 | $normalizedMappings = $this->normalizeMappings( $mappingsRequest ); 54 | 55 | $invalidMappings = $this->getInvalidMappings( $normalizedMappings ); 56 | if ( $invalidMappings !== [] ) { 57 | $this->presenter->presentInvalidMappings( $invalidMappings ); 58 | return; 59 | } 60 | 61 | $mappings = $this->mappingListSerializer->mappingListFromArray( $normalizedMappings ); 62 | 63 | try { 64 | $this->repository->setMappings( $entityId, $mappings ); 65 | $this->presenter->presentSuccess(); 66 | } catch ( Throwable ) { 67 | $this->presenter->presentSaveFailed(); 68 | } 69 | } 70 | 71 | /** 72 | * @param array $mappings 73 | */ 74 | private function mappingsIsList( array $mappings ): bool { 75 | return count( array_filter( array_keys( $mappings ), 'is_string' ) ) === 0; 76 | } 77 | 78 | /** 79 | * @param array $mappings 80 | * 81 | * @return array 82 | */ 83 | private function normalizeMappings( array $mappings ): array { 84 | $normalized = []; 85 | foreach ( $mappings as $mapping ) { 86 | if ( !is_array( $mapping ) ) { 87 | $normalized[] = [ 'predicate' => '', 'object' => '' ]; 88 | continue; 89 | } 90 | $normalized[] = [ 91 | self::PREDICATE_KEY => (string)( $mapping[self::PREDICATE_KEY] ?? '' ), 92 | self::OBJECT_KEY => (string)( $mapping[self::OBJECT_KEY] ?? '' ), 93 | ]; 94 | } 95 | return $normalized; 96 | } 97 | 98 | /** 99 | * @param array $mappings 100 | * 101 | * @return array 102 | */ 103 | private function getInvalidMappings( array $mappings ): array { 104 | // TOOD: we might want to keep the original index to quickly identify the row when getting this in the UI JS. 105 | return array_values( 106 | array_filter( 107 | $mappings, 108 | fn ( array $mapping ) => !$this->mappingIsValid( $mapping[self::PREDICATE_KEY], $mapping[self::OBJECT_KEY] ) 109 | ) 110 | ); 111 | } 112 | 113 | private function mappingIsValid( string $predicate, string $object ): bool { 114 | return $predicate !== '' 115 | && $object !== '' 116 | && str_contains( $predicate, ':' ) 117 | && $this->allowedPredicates->contains( $predicate ) 118 | && $this->objectValidator->isValid( $object ); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Application/WikibaseUrlObjectValidator.php: -------------------------------------------------------------------------------- 1 | validators as $validator ) { 23 | $result = $validator->validate( $value ); 24 | if ( !$result->isValid() ) { 25 | return false; 26 | } 27 | } 28 | return true; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/DataAccess/CombiningMappingPredicatesLookup.php: -------------------------------------------------------------------------------- 1 | localSettingLookup->getMappingPredicates()->plus( $this->wikiLookup->getMappingPredicates() ); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/DataAccess/LocalSettingsMappingPredicatesLookup.php: -------------------------------------------------------------------------------- 1 | $predicates 15 | */ 16 | public function __construct( 17 | private array $predicates 18 | ) { 19 | } 20 | 21 | public function getMappingPredicates(): PredicateList { 22 | $predicatesList = []; 23 | foreach ( $this->predicates as $predicate ) { 24 | if ( !is_string( $predicate ) ) { 25 | continue; 26 | } 27 | try { 28 | $predicatesList[] = new Predicate( $predicate ); 29 | } catch ( InvalidArgumentException ) { 30 | } 31 | } 32 | return new PredicateList( $predicatesList ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/DataAccess/MappingPredicatesLookup.php: -------------------------------------------------------------------------------- 1 | titleParser = $titleParser; 16 | $this->revisionLookup = $revisionLookup; 17 | } 18 | 19 | public function getPageContent( string $pageTitle ): ?\Content { 20 | try { 21 | $title = $this->titleParser->parseTitle( $pageTitle ); 22 | } catch ( \MalformedTitleException $e ) { 23 | return null; 24 | } 25 | 26 | $revision = $this->revisionLookup->getRevisionByTitle( $title ); 27 | 28 | return $revision?->getContent( 'main' ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/DataAccess/PredicatesDeserializer.php: -------------------------------------------------------------------------------- 1 | validator->validate( $predicatesText ) ) { 19 | return new PredicateList(); 20 | } 21 | 22 | return new PredicateList( 23 | array_values( 24 | array_map( 25 | fn ( string $predicate ) => new Predicate( $predicate ), 26 | $this->validator->getValidPredicates() 27 | ) 28 | ) 29 | ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/DataAccess/PredicatesTextValidator.php: -------------------------------------------------------------------------------- 1 | normalizeLines( $predicatesText ); 21 | 22 | foreach ( $lines as $line ) { 23 | if ( $this->predicateIsValid( $line ) ) { 24 | $this->validPredicates[] = $line; 25 | } else { 26 | $this->invalidPredicates[] = $line; 27 | } 28 | } 29 | 30 | return $this->invalidPredicates === []; 31 | } 32 | 33 | /** 34 | * @return string[] 35 | */ 36 | private function normalizeLines( string $predicatesText ): array { 37 | return array_filter( 38 | array_map( 39 | fn ( string $predicate ) => trim( $predicate ), 40 | explode( "\n", $predicatesText ) 41 | ) 42 | ); 43 | } 44 | 45 | private function predicateIsValid( string $predicate ): bool { 46 | return str_contains( $predicate, ':' ); 47 | } 48 | 49 | /** 50 | * @return string[] 51 | */ 52 | public function getValidPredicates(): array { 53 | return $this->validPredicates; 54 | } 55 | 56 | /** 57 | * @return string[] 58 | */ 59 | public function getInvalidPredicates(): array { 60 | return $this->invalidPredicates; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/DataAccess/WikiMappingPredicatesLookup.php: -------------------------------------------------------------------------------- 1 | contentFetcher->getPageContent( 'MediaWiki:' . $this->pageName ); 20 | 21 | if ( $content instanceof \TextContent ) { 22 | return $this->predicatesFromTextContent( $content ); 23 | } 24 | 25 | return new PredicateList(); 26 | } 27 | 28 | private function predicatesFromTextContent( \TextContent $content ): PredicateList { 29 | return $this->deserializer->deserialize( $content->getText() ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/EntryPoints/MediaWikiHooks.php: -------------------------------------------------------------------------------- 1 | addServiceManipulator( 30 | 'SlotRoleRegistry', 31 | static function ( SlotRoleRegistry $registry ) { 32 | $registry->defineRoleWithModel( 33 | WikibaseRdfExtension::SLOT_NAME, 34 | CONTENT_MODEL_JSON, 35 | [ 'display' => 'none' ] 36 | ); 37 | } 38 | ); 39 | } 40 | 41 | // TODO: is this the best hook? 42 | public static function onOutputPageParserOutput( OutputPage $page, ParserOutput $parserOutput ): void { 43 | $entityId = self::getEntityIdForOutputPage( $page ); 44 | 45 | if ( $entityId !== null ) { 46 | self::addMappingUi( $page, $entityId ); 47 | } 48 | } 49 | 50 | private static function getEntityIdForOutputPage( OutputPage $page ): ?EntityId { 51 | $title = $page->getTitle(); 52 | 53 | if ( $title === null 54 | || !$title->inNamespaces( WB_NS_ITEM, WB_NS_PROPERTY ) 55 | || !$title->exists() ) { 56 | return null; 57 | } 58 | 59 | try { 60 | return WikibaseRepo::getEntityIdParser()->parse( $title->getText() ); 61 | } catch ( EntityIdParsingException ) { 62 | return null; 63 | } 64 | } 65 | 66 | private static function addMappingUi( OutputPage $page, EntityId $entityId ): void { 67 | // TODO: load styles earlier because this causes the initial content to be unstyled 68 | $page->addModules( [ 'ext.wikibase.rdf' ] ); 69 | 70 | $presenter = WikibaseRdfExtension::getInstance()->newHtmlMappingsPresenter( 71 | $page->getRequest()->getCheck( 'diff' ) 72 | ); 73 | 74 | $repository = WikibaseRdfExtension::getInstance()->newMappingRepository( $page->getUser() ); 75 | $authorizer = WikibaseRdfExtension::getInstance()->newEntityMappingsAuthorizer( $page->getUser() ); 76 | 77 | $presenter->showMappings( 78 | $repository->getMappings( $entityId, $page->getRevisionId() ?? 0 ), 79 | $authorizer->canEditEntityMappings( $entityId ) 80 | ); 81 | 82 | $page->addHTML( $presenter->getHtml() ); 83 | } 84 | 85 | /** 86 | * @param array> $entityTypeDefinitions 87 | */ 88 | public static function onWikibaseRepoEntityTypes( array &$entityTypeDefinitions ): void { 89 | $entityTypeDefinitions['item'][EntityTypeDefinitions::RDF_BUILDER_FACTORY_CALLBACK] 90 | = self::newEntityRdfBuilderFactoryFunction( $entityTypeDefinitions['item'][EntityTypeDefinitions::RDF_BUILDER_FACTORY_CALLBACK] ); 91 | 92 | $entityTypeDefinitions['property'][EntityTypeDefinitions::RDF_BUILDER_FACTORY_CALLBACK] 93 | = self::newEntityRdfBuilderFactoryFunction( $entityTypeDefinitions['property'][EntityTypeDefinitions::RDF_BUILDER_FACTORY_CALLBACK] ); 94 | } 95 | 96 | private static function newEntityRdfBuilderFactoryFunction( callable $factoryFunction ): callable { 97 | return fn ( 98 | int $flavorFlags, 99 | RdfVocabulary $vocabulary, 100 | RdfWriter $writer 101 | ): EntityRdfBuilder => new MultiEntityRdfBuilder( 102 | $factoryFunction( ...func_get_args() ), 103 | WikibaseRdfExtension::getInstance()->newMappingRdfBuilder( $writer ) 104 | ); 105 | } 106 | 107 | public static function onContentHandlerDefaultModelFor( Title $title, ?string &$model ): void { 108 | if ( WikibaseRdfExtension::getInstance()->isConfigTitle( $title ) ) { 109 | $model = CONTENT_MODEL_TEXT; 110 | } 111 | } 112 | 113 | public static function onEditFilter( EditPage $editPage, ?string $text, ?string $section, string &$error ): void { 114 | if ( is_string( $text ) && WikibaseRdfExtension::getInstance()->isConfigTitle( $editPage->getTitle() ) ) { 115 | $validator = new PredicatesTextValidator(); 116 | if ( !$validator->validate( $text ) ) { 117 | $error = self::createInvalidPredicatesError( $validator->getInvalidPredicates() ); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * @param string[] $invalidPredicates 124 | */ 125 | private static function createInvalidPredicatesError( array $invalidPredicates ): string { 126 | $imploded = implode( 127 | ', ', 128 | array_map( 129 | fn ( string $predicate ) => '"' . $predicate . '"', 130 | $invalidPredicates 131 | ) 132 | ); 133 | return \Html::errorBox( 134 | wfMessage( 'wikibase-rdf-config-invalid', count( $invalidPredicates ), $imploded )->escaped() 135 | ); 136 | } 137 | 138 | public static function onAlternateEdit( EditPage $editPage ): void { 139 | if ( WikibaseRdfExtension::getInstance()->isConfigTitle( $editPage->getTitle() ) ) { 140 | $editPage->suppressIntro = true; 141 | $editPage->editFormTextBeforeContent = wfMessage( 'wikibase-rdf-config-intro' )->parse(); 142 | } 143 | } 144 | 145 | public static function onArticleRevisionViewCustom( 146 | RevisionRecord $revision, 147 | Title $title, 148 | int $oldId, 149 | OutputPage $output 150 | ): bool { 151 | if ( WikibaseRdfExtension::getInstance()->isConfigTitle( $title ) ) { 152 | $localSettingsLookup = WikibaseRdfExtension::getInstance()->newLocalSettingsMappingPredicatesLookup(); 153 | $wikiSettingsLookup = WikibaseRdfExtension::getInstance()->newWikiMappingPredicatesLookup(); 154 | $presenter = WikibaseRdfExtension::getInstance()->newHtmlPredicatesPresenter(); 155 | $presenter->presentPredicates( 156 | $localSettingsLookup->getMappingPredicates(), 157 | $wikiSettingsLookup->getMappingPredicates() 158 | ); 159 | $output->clearHTML(); 160 | $output->addHTML( $presenter->getHtml() ); 161 | return false; 162 | } 163 | return true; 164 | } 165 | 166 | /** 167 | * @param array $preferences 168 | */ 169 | public static function onGetPreferences( User $user, array &$preferences ): void { 170 | $preferences['wikibase-rdf-acknowledge-property-edit'] = [ 171 | 'type' => 'api' 172 | ]; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/EntryPoints/Rest/GetAllMappingsApi.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function run(): array { 24 | $mappings = $this->allMappingsLookup->getAllMappings(); 25 | 26 | return [ 27 | 'mappings' => $this->entityMappingsToArray( $mappings ), 28 | ]; 29 | } 30 | 31 | /** 32 | * @param MappingListAndId[] $entityMappingsList 33 | * 34 | * @return array> 35 | */ 36 | private function entityMappingsToArray( array $entityMappingsList ): array { 37 | $array = []; 38 | foreach ( $entityMappingsList as $entityMappings ) { 39 | $array[$entityMappings->entityId->getSerialization()] = $this->serializer->mappingListToArray( $entityMappings->mappingList ); 40 | } 41 | return $array; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/EntryPoints/Rest/GetMappingsApi.php: -------------------------------------------------------------------------------- 1 | getEntityId( $entityId ); 30 | } catch ( EntityIdParsingException ) { 31 | return $this->presentInvalidEntityId(); 32 | } 33 | 34 | return $this->presentMappings( $this->getMappings( $realEntityId ) ); 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function needsWriteAccess() { 41 | return false; 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | * @return array> 47 | */ 48 | public function getParamSettings(): array { 49 | return [ 50 | 'entity_id' => [ 51 | self::PARAM_SOURCE => 'path', 52 | ParamValidator::PARAM_TYPE => 'string', 53 | ParamValidator::PARAM_REQUIRED => true, 54 | ], 55 | ]; 56 | } 57 | 58 | private function getEntityId( string $entityId ): EntityId { 59 | return $this->entityIdParser->parse( $entityId ); 60 | } 61 | 62 | private function getMappings( EntityId $entityId ): MappingList { 63 | return $this->mappingRepository->getMappings( $entityId ); 64 | } 65 | 66 | /** 67 | * @return array> 68 | */ 69 | private function getMappingsArray( MappingList $mappings ): array { 70 | return $this->mappingListSerializer->mappingListToArray( $mappings ); 71 | } 72 | 73 | public function presentInvalidEntityId(): Response { 74 | return $this->getResponseFactory()->createLocalizedHttpError( 75 | 400, 76 | MessageValue::new( 'wikibase-rdf-entity-id-invalid' ), 77 | ); 78 | } 79 | 80 | public function presentMappings( MappingList $mappings ): Response { 81 | return $this->getResponseFactory()->createJson( [ 82 | 'mappings' => $this->getMappingsArray( $mappings ) 83 | ] ); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/EntryPoints/Rest/SaveMappingsApi.php: -------------------------------------------------------------------------------- 1 | newRestSaveMappingsPresenter( $this->getResponseFactory() ); 18 | $useCase = WikibaseRdfExtension::getInstance()->newSaveMappingsUseCase( $presenter, $this->getUser() ); 19 | 20 | $body = $this->getValidatedBody(); 21 | $mappings = ( is_array( $body ) && isset( $body['mappings'] ) ) 22 | ? $body['mappings'] 23 | : []; 24 | 25 | $useCase->saveMappings( $entityId, $mappings ); 26 | 27 | return $presenter->getResponse(); 28 | } 29 | 30 | private function getUser(): User { 31 | return RequestContext::getMain()->getUser(); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | * @return array> 37 | */ 38 | public function getParamSettings(): array { 39 | return [ 40 | 'entity_id' => [ 41 | self::PARAM_SOURCE => 'path', 42 | ParamValidator::PARAM_TYPE => 'string', 43 | ParamValidator::PARAM_REQUIRED => true, 44 | ], 45 | ]; 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | * @return array> 51 | */ 52 | public function getBodyParamSettings(): array { 53 | return [ 54 | 'mappings' => [ 55 | self::PARAM_SOURCE => 'body', 56 | ParamValidator::PARAM_TYPE => 'array', 57 | ParamValidator::PARAM_REQUIRED => true 58 | ], 59 | ]; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/MappingListSerializer.php: -------------------------------------------------------------------------------- 1 | validMappingListFromArray( $array ); 21 | } 22 | 23 | return new MappingList(); 24 | } 25 | 26 | /** 27 | * @param array $mappings 28 | */ 29 | public function mappingListFromArray( array $mappings ): MappingList { 30 | return new MappingList( 31 | array_map( 32 | fn ( array $mapping ) => $this->mappingFromArray( $mapping ), 33 | $mappings 34 | ) 35 | ); 36 | } 37 | 38 | /** 39 | * @param array $mappings 40 | */ 41 | public function validMappingListFromArray( array $mappings ): MappingList { 42 | $mappingObjects = []; 43 | foreach ( $mappings as $mapping ) { 44 | try { 45 | $mappingObjects[] = $this->mappingFromArray( $mapping ); 46 | } catch ( InvalidArgumentException ) { 47 | } 48 | } 49 | return new MappingList( $mappingObjects ); 50 | } 51 | 52 | /** 53 | * @param array{predicate: string, object: string} $mapping 54 | */ 55 | private function mappingFromArray( array $mapping ): Mapping { 56 | return new Mapping( 57 | predicate: $mapping[self::PREDICATE_KEY], 58 | object: $mapping[self::OBJECT_KEY] 59 | ); 60 | } 61 | 62 | public function toJson( MappingList $mappingList ): string { 63 | return (string)json_encode( $this->mappingListToArray( $mappingList ) ); 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function mappingListToArray( MappingList $mappings ): array { 70 | return array_map( 71 | fn ( Mapping $mapping ) => [ 72 | self::PREDICATE_KEY => $mapping->predicate, 73 | self::OBJECT_KEY => $mapping->object 74 | ], 75 | $mappings->asArray() 76 | ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Persistence/ContentSlotMappingRepository.php: -------------------------------------------------------------------------------- 1 | contentRepository->getContent( $entityId, $revisionId ); 23 | 24 | if ( $content instanceof JsonContent ) { 25 | return $this->serializer->fromJson( $content->getText() ); 26 | } 27 | 28 | return new MappingList(); 29 | } 30 | 31 | public function setMappings( EntityId $entityId, MappingList $mappingList ): void { 32 | $this->contentRepository->setContent( 33 | $entityId, 34 | $this->mappingListToContent( $mappingList ) 35 | ); 36 | } 37 | 38 | private function mappingListToContent( MappingList $mappingList ): JsonContent { 39 | return new JsonContent( $this->serializer->toJson( $mappingList ) ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Persistence/EntityContentRepository.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $contentList = []; 16 | 17 | public function getContent( EntityId $entityId, int $revisionId = 0 ): ?Content { 18 | return $this->contentList[$entityId->getSerialization()] ?? null; 19 | } 20 | 21 | public function setContent( EntityId $entityId, Content $content ): void { 22 | $this->contentList[$entityId->getSerialization()] = $content; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Persistence/InMemoryMappingRepository.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $mappingsById = []; 17 | 18 | public function getMappings( EntityId $entityId, int $revisionId = 0 ): MappingList { 19 | return $this->mappingsById[$entityId->getSerialization()] ?? new MappingList(); 20 | } 21 | 22 | public function setMappings( EntityId $entityId, MappingList $mappingList ): void { 23 | $this->mappingsById[$entityId->getSerialization()] = $mappingList; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Persistence/SlotEntityContentRepository.php: -------------------------------------------------------------------------------- 1 | getWikiPage( $entityId ); 32 | 33 | if ( $page === null ) { 34 | return null; 35 | } 36 | 37 | $revision = $this->revisionLookup->getRevisionByTitle( $page, $revisionId ); 38 | 39 | try { 40 | return $revision?->getSlot( $this->slotName, RevisionRecord::FOR_PUBLIC, $this->authority )?->getContent(); 41 | } catch ( RevisionAccessException ) { 42 | return null; 43 | } 44 | } 45 | 46 | private function getWikiPage( EntityId $entityId ): ?WikiPage { 47 | try { 48 | $title = $this->entityTitleLookup->getTitleForId( $entityId ); 49 | } catch ( Exception ) { 50 | return null; 51 | } 52 | 53 | if ( $title === null ) { 54 | return null; 55 | } 56 | 57 | return $this->pageFactory->newFromTitle( $title ); 58 | } 59 | 60 | public function setContent( EntityId $entityId, Content $content ): void { 61 | $page = $this->getWikiPage( $entityId ); 62 | 63 | if ( $page !== null ) { 64 | $updater = $page->newPageUpdater( $this->authority ); 65 | $updater->setContent( $this->slotName, $content ); 66 | $updater->saveRevision( CommentStoreComment::newUnsavedComment( 'Updated entity mappings' ) ); // TODO 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Persistence/SqlAllMappingsLookup.php: -------------------------------------------------------------------------------- 1 | resultsToEntityMappingList( $this->fetchResults() ); 29 | } 30 | 31 | private function fetchResults(): IResultWrapper { 32 | return $this->database->newSelectQueryBuilder() 33 | ->select( [ 34 | 't.old_text', 35 | 'p.page_title', 36 | ] ) 37 | ->from( 'text', 't' ) 38 | ->join( 'slots', 's', 's.slot_content_id=t.old_id' ) 39 | ->join( 'slot_roles', 'r', 'r.role_id=s.slot_role_id' ) 40 | ->join( 'page', 'p', 'p.page_latest=s.slot_revision_id' ) 41 | ->where( [ 'r.role_name' => $this->slotName ] ) 42 | ->caller( __METHOD__ ) 43 | ->fetchResultSet(); 44 | } 45 | 46 | /** 47 | * @return MappingListAndId[] 48 | */ 49 | private function resultsToEntityMappingList( IResultWrapper $results ): array { 50 | $entityMappingsList = []; 51 | foreach ( $results as $row ) { 52 | if ( !is_object( $row ) ) { 53 | continue; 54 | } 55 | $entityMappingsList[] = new MappingListAndId( 56 | $this->entityIdParser->parse( (string)$row->page_title ), 57 | $this->serializer->fromJson( (string)$row->old_text ) 58 | ); 59 | } 60 | return $entityMappingsList; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Persistence/UserBasedEntityMappingsAuthorizer.php: -------------------------------------------------------------------------------- 1 | permissionChecker->getPermissionStatusForEntityId( 22 | $this->user, 23 | EntityPermissionChecker::ACTION_EDIT, 24 | $entityId 25 | )->isGood(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Presentation/HtmlMappingsPresenter.php: -------------------------------------------------------------------------------- 1 | response = ''; 38 | } 39 | 40 | private function createHeader(): string { 41 | return '' 42 | . '' . $this->msg( 'wikibase-rdf-mappings-predicate-heading' )->escaped() . '' 43 | . '' . $this->msg( 'wikibase-rdf-mappings-object-heading' )->escaped() . '' 44 | . '' 45 | . ''; 46 | } 47 | 48 | private function createErrorBox(): string { 49 | return ''; 50 | } 51 | 52 | private function createEditTemplate( bool $canEdit ): string { 53 | if ( !$canEdit ) { 54 | return ''; 55 | } 56 | return '' 57 | . '' . $this->createPredicateSelect() . '' 58 | . '' 59 | . '' 60 | . '' . $this->msg( 'wikibase-rdf-mappings-action-save' )->escaped() . ' ' 61 | . '' . $this->msg( 'wikibase-rdf-mappings-action-remove' )->escaped() . ' ' 62 | . '' . $this->msg( 'wikibase-rdf-mappings-action-cancel' )->escaped() . '' 63 | . '' 64 | . ''; 65 | } 66 | 67 | private function createRowTemplate( bool $canEdit ): string { 68 | return '' 69 | . '' 70 | . '' 71 | . '' . $this->createEditButton( $canEdit ) . '' 72 | . ''; 73 | } 74 | 75 | private function createRows( MappingList $mappingList, bool $canEdit ): string { 76 | $html = ''; 77 | foreach ( $mappingList->asArray() as $mapping ) { 78 | $html .= $this->createRow( $mapping->predicate, $mapping->object, $canEdit ); 79 | } 80 | 81 | return $html; 82 | } 83 | 84 | private function createPredicateSelect(): string { 85 | $html = ''; 90 | return $html; 91 | } 92 | 93 | private function createRow( string $relationship, string $url, bool $canEdit ): string { 94 | return Html::rawElement( 95 | 'tr', 96 | [ 'class' => 'wikibase-rdf-row', 'data-predicate' => $relationship, 'data-object' => $url ], 97 | Html::element( 'td', [ 'class' => 'wikibase-rdf-predicate' ], $relationship ) 98 | . Html::rawElement( 'td', [ 'class' => 'wikibase-rdf-object' ], $this->createObjectLink( $url ) ) 99 | . Html::rawElement( 'td', [ 'class' => 'wikibase-rdf-actions' ], $this->createEditButton( $canEdit ) ) 100 | ); 101 | } 102 | 103 | private function createObjectLink( string $url ): string { 104 | return Html::element( 'a', [ 'href' => $url, 'rel' => 'nofollow', 'class' => 'external' ], $url ); 105 | } 106 | 107 | private function createEditButton( bool $canEdit ): string { 108 | if ( !$canEdit ) { 109 | return ''; 110 | } 111 | return '' . $this->msg( 'wikibase-rdf-mappings-action-edit' )->escaped() . ''; 112 | } 113 | 114 | private function createFooter( bool $canEdit ): string { 115 | if ( !$canEdit ) { 116 | return ''; 117 | } 118 | return ''; 121 | } 122 | 123 | public function getHtml(): string { 124 | return $this->response; 125 | } 126 | 127 | private function msg( string $key, mixed ...$params ): Message { 128 | return new Message( $key, $params ); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/Presentation/HtmlPredicatesPresenter.php: -------------------------------------------------------------------------------- 1 | *'; 15 | 16 | private string $response = ''; 17 | private PredicateList $localSettingsPredicates; 18 | private PredicateList $wikiPredicates; 19 | 20 | public function presentPredicates( PredicateList $localSettingsPredicates, PredicateList $wikiPredicates ): void { 21 | $this->localSettingsPredicates = $localSettingsPredicates; 22 | $this->wikiPredicates = $wikiPredicates; 23 | $this->response = $this->createIntro() . $this->createList() . $this->createFooter(); 24 | } 25 | 26 | private function createIntro(): string { 27 | return Html::rawElement( 'p', [], $this->msg( 'wikibase-rdf-config-list-intro' )->parse() ); 28 | } 29 | 30 | private function createList(): string { 31 | $localSettingsPredicates = $this->localSettingsPredicates->asArray(); 32 | $wikiPredicates = $this->wikiPredicates->asArray(); 33 | $count = count( $localSettingsPredicates ) + count( $wikiPredicates ); 34 | 35 | if ( $count === 0 ) { 36 | return Html::element( 'p', [], $this->msg( 'wikibase-rdf-config-list-empty' )->text() ); 37 | } 38 | 39 | return Html::element( 'p', [], $this->msg( 'wikibase-rdf-config-list-heading', $count )->text() ) 40 | . Html::rawElement( 41 | 'ul', 42 | [], 43 | implode( $this->createLocalSettingsListItems() ) . implode( $this->createWikiListItems() ) 44 | ); 45 | } 46 | 47 | /** 48 | * @return string[] 49 | */ 50 | private function createLocalSettingsListItems(): array { 51 | return array_map( 52 | static function ( Predicate $predicate ) { 53 | return Html::rawElement( 54 | 'li', 55 | [], 56 | Html::element( 'span', [], $predicate->predicate ) . ' ' . self::ASTERISK 57 | ); 58 | }, 59 | $this->localSettingsPredicates->asArray() 60 | ); 61 | } 62 | 63 | /** 64 | * @return string[] 65 | */ 66 | private function createWikiListItems(): array { 67 | return array_map( 68 | fn ( Predicate $predicate ) => Html::element( 'li', [], $predicate->predicate ), 69 | $this->wikiPredicates->asArray() 70 | ); 71 | } 72 | 73 | private function createFooter(): string { 74 | if ( count( $this->localSettingsPredicates->asArray() ) === 0 ) { 75 | return ''; 76 | } 77 | 78 | return Html::rawElement( 79 | 'p', 80 | [], 81 | self::ASTERISK . ' ' . $this->msg( 'wikibase-rdf-config-list-footer' )->escaped() 82 | ); 83 | } 84 | 85 | public function getHtml(): string { 86 | return $this->response; 87 | } 88 | 89 | private function msg( string $key, mixed ...$params ): Message { 90 | return new Message( $key, $params ); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/Presentation/MappingsPresenter.php: -------------------------------------------------------------------------------- 1 | getMappings( $entity ); 26 | 27 | if ( $mappings === [] ) { 28 | return; 29 | } 30 | 31 | // $this->writer->about( 32 | // $this->vocabulary->entityNamespaceNames[$this->vocabulary->getEntityRepositoryName( $entity->getId() )], 33 | // $this->vocabulary->getEntityLName( $entity->getId() ) 34 | // ); 35 | 36 | if ( $entity->getType() === Property::ENTITY_TYPE ) { 37 | $this->addPropertyMappings( $mappings, $entity ); 38 | } else { 39 | $this->addItemMappings( $mappings ); 40 | } 41 | } 42 | 43 | /** 44 | * @param Mapping[] $mappings 45 | */ 46 | private function addItemMappings( array $mappings ): void { 47 | foreach ( $mappings as $mapping ) { 48 | $this->addItemMapping( $mapping ); 49 | } 50 | } 51 | 52 | /** 53 | * @param Mapping[] $mappings 54 | */ 55 | private function addPropertyMappings( array $mappings, EntityDocument $entity ): void { 56 | $id = $entity->getId(); 57 | 58 | if ( $id === null ) { 59 | return; 60 | } 61 | 62 | foreach ( $mappings as $mapping ) { 63 | $this->addPropertyMapping( $mapping, $id ); 64 | } 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | private function getMappings( EntityDocument $entity ): array { 71 | $id = $entity->getId(); 72 | 73 | if ( $id === null ) { 74 | return []; 75 | } 76 | 77 | return $this->repository->getMappings( $id )->asArray(); 78 | } 79 | 80 | private function addItemMapping( Mapping $mapping ): void { 81 | $this->writer 82 | ->say( $mapping->getPredicateBase(), $mapping->getPredicateLocal() ) 83 | ->is( $mapping->object ); 84 | } 85 | 86 | private function addPropertyMapping( Mapping $mapping, EntityId $entityId ): void { 87 | $this->writer 88 | ->about( $this->propertyMappingPrefixBuilder->getPrefix(), $entityId->getSerialization() ) 89 | ->a( 'owl', 'ObjectProperty' ) 90 | ->say( $mapping->getPredicateBase(), $mapping->getPredicateLocal() ) 91 | ->is( $mapping->object ); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Presentation/RDF/MultiEntityRdfBuilder.php: -------------------------------------------------------------------------------- 1 | builders = $builders; 21 | } 22 | 23 | public function addEntity( EntityDocument $entity ): void { 24 | foreach ( $this->builders as $builder ) { 25 | $builder->addEntity( $entity ); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Presentation/RDF/PropertyMappingPrefixBuilder.php: -------------------------------------------------------------------------------- 1 | rdfNodeNamespacePrefix . RdfVocabulary::NSP_DIRECT_CLAIM; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Presentation/RestSaveMappingsPresenter.php: -------------------------------------------------------------------------------- 1 | response = $this->responseFactory->createNoContent(); 23 | } 24 | 25 | public function presentInvalidMappings( array $mappings ): void { 26 | $this->response = $this->responseFactory->createLocalizedHttpError( 27 | 400, 28 | MessageValue::new( 'wikibase-rdf-save-mappings-invalid-mappings' ), 29 | [ 'invalidMappings' => $mappings ] 30 | ); 31 | } 32 | 33 | public function presentSaveFailed(): void { 34 | $this->response = $this->responseFactory->createLocalizedHttpError( 35 | 500, 36 | MessageValue::new( 'wikibase-rdf-save-mappings-save-failed' ) 37 | ); 38 | } 39 | 40 | public function presentInvalidEntityId(): void { 41 | $this->response = $this->responseFactory->createLocalizedHttpError( 42 | 400, 43 | MessageValue::new( 'wikibase-rdf-entity-id-invalid' ), 44 | ); 45 | } 46 | 47 | public function presentPermissionDenied(): void { 48 | $this->response = $this->responseFactory->createLocalizedHttpError( 49 | 403, 50 | MessageValue::new( 'wikibase-rdf-permission-denied' ), 51 | ); 52 | } 53 | 54 | public function getResponse(): Response { 55 | return $this->response; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/SpecialMappingPredicates.php: -------------------------------------------------------------------------------- 1 | getOutput()->redirect( $title->getFullURL() ); 24 | } 25 | } 26 | 27 | public function getGroupName(): string { 28 | return 'wikibase'; 29 | } 30 | 31 | public function getDescription(): Message { 32 | return $this->msg( 'special-mapping-predicates' ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Application/MappingListUnitTest.php: -------------------------------------------------------------------------------- 1 | assertSame( [], $list->asArray() ); 20 | } 21 | 22 | public function testCanAccessMappingsAsArray(): void { 23 | $mappingArray = [ 24 | new Mapping( 'owl:sameAs', 'https://example.com/uri/1' ), 25 | new Mapping( 'rdfs:subClassOf', 'https://example.com/uri/2' ) 26 | ]; 27 | 28 | $list = new MappingList( $mappingArray ); 29 | 30 | $this->assertEquals( $mappingArray, $list->asArray() ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Application/MappingTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 20 | 'owl', 21 | $mapping->getPredicateBase() 22 | ); 23 | } 24 | 25 | public function testGetPredicateLocal(): void { 26 | $mapping = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 27 | 28 | $this->assertSame( 29 | 'sameAs', 30 | $mapping->getPredicateLocal() 31 | ); 32 | } 33 | 34 | public function testInvalidPredicateThrowsException(): void { 35 | $this->expectException( InvalidArgumentException::class ); 36 | 37 | new Mapping( 'notValid', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 38 | } 39 | 40 | public function provideValidObjects(): iterable { 41 | yield 'Standard URL' => [ 'http://www.w3.org/2002/07/owl#sameAs' ]; 42 | yield 'Other protocol' => [ 'ftp://example.com' ]; 43 | yield 'Without TLD' => [ 'http://example' ]; 44 | yield 'Non-ASCII' => [ 'http://exåmple.com' ]; 45 | } 46 | 47 | /** 48 | * @dataProvider provideValidObjects 49 | */ 50 | public function testValidObject( string $object ): void { 51 | $mapping = new Mapping( 'owl:sameAs', $object ); 52 | 53 | $this->assertSame( $object, $mapping->object ); 54 | } 55 | 56 | public static function provideInvalidObjects(): iterable { 57 | yield 'Missing protocol' => [ 'example.com' ]; 58 | yield 'Missing slash' => [ 'http:/example.com' ]; 59 | } 60 | 61 | /** 62 | * @dataProvider provideInvalidObjects 63 | */ 64 | public function testInvalidObjectThrowsException( string $object ): void { 65 | $this->expectException( InvalidArgumentException::class ); 66 | 67 | new Mapping( 'owl:sameAs', $object ); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /tests/Application/PredicateListTest.php: -------------------------------------------------------------------------------- 1 | assertSame( [], $list->asArray() ); 20 | } 21 | 22 | public function testCanAccessPredicatesAsArray(): void { 23 | $predicatesArray = [ 24 | new Predicate( 'owl:sameAs' ), 25 | new Predicate( 'rdfs:subClassOf' ) 26 | ]; 27 | 28 | $list = new PredicateList( $predicatesArray ); 29 | 30 | $this->assertEquals( $predicatesArray, $list->asArray() ); 31 | } 32 | 33 | public function testCanCombineLists(): void { 34 | $predicatesArray1 = [ 35 | new Predicate( 'owl:sameAs' ), 36 | new Predicate( 'rdfs:subClassOf' ) 37 | ]; 38 | 39 | $list1 = new PredicateList( $predicatesArray1 ); 40 | 41 | $predicatesArray2 = [ 42 | new Predicate( 'foo:bar' ), 43 | new Predicate( 'bar:baz' ) 44 | ]; 45 | 46 | $list2 = new PredicateList( $predicatesArray2 ); 47 | 48 | $allList = new PredicateList( array_merge( $predicatesArray1, $predicatesArray2 ) ); 49 | 50 | $this->assertEquals( $allList, $list1->plus( $list2 ) ); 51 | } 52 | 53 | public function testContainsPredicate(): void { 54 | $list = new PredicateList( [ 55 | new Predicate( 'owl:sameAs' ), 56 | new Predicate( 'rdfs:subClassOf' ) 57 | ] ); 58 | 59 | $this->assertTrue( $list->contains( 'rdfs:subClassOf' ) ); 60 | } 61 | 62 | public function testDoesNotContainPredicate(): void { 63 | $list = new PredicateList( [ 64 | new Predicate( 'owl:sameAs' ), 65 | new Predicate( 'rdfs:subClassOf' ) 66 | ] ); 67 | 68 | $this->assertFalse( $list->contains( 'foo:bar' ) ); 69 | } 70 | 71 | public function testFiltersDuplicates(): void { 72 | $list = new PredicateList( [ 73 | new Predicate( 'foo:bar' ), 74 | new Predicate( 'owl:sameAs' ), 75 | new Predicate( 'rdfs:subClassOf' ), 76 | new Predicate( 'owl:sameAs' ), 77 | new Predicate( 'owl:sameAs' ), 78 | ] ); 79 | 80 | $this->assertEquals( 81 | [ 82 | new Predicate( 'foo:bar' ), 83 | new Predicate( 'owl:sameAs' ), 84 | new Predicate( 'rdfs:subClassOf' ), 85 | ], 86 | $list->asArray() 87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /tests/Application/PredicateTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 'foo:bar', $predicate->predicate ); 19 | } 20 | 21 | public function testConstructorThrowsException(): void { 22 | $this->expectException( InvalidArgumentException::class ); 23 | new Predicate( 'notvalid' ); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/Application/SaveMappings/SaveMappingsUseCaseTest.php: -------------------------------------------------------------------------------- 1 | presenter = new SpySaveMappingsPresenter(); 39 | $this->repository = new InMemoryMappingRepository(); 40 | } 41 | 42 | private function newSucceedingAuthorizer(): EntityMappingsAuthorizer { 43 | return new SucceedingEntityMappingsAuthorizer(); 44 | } 45 | 46 | private function newFailingAuthorizer(): EntityMappingsAuthorizer { 47 | return new FailingEntityMappingsAuthorizer(); 48 | } 49 | 50 | private function newUseCase( EntityMappingsAuthorizer $authorizer ): SaveMappingsUseCase { 51 | return new SaveMappingsUseCase( 52 | $this->presenter, 53 | $this->repository, 54 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ), 55 | new BasicEntityIdParser(), 56 | new MappingListSerializer(), 57 | $authorizer, 58 | new SucceedingObjectValidator() 59 | ); 60 | } 61 | 62 | public function testShouldShowSuccess(): void { 63 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() ); 64 | 65 | $useCase->saveMappings( 66 | 'Q1', 67 | [ 68 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ] 69 | ] 70 | ); 71 | 72 | $this->assertTrue( $this->presenter->showedSuccess ); 73 | $this->assertSame( [], $this->presenter->invalidMappings ); 74 | $this->assertFalse( $this->presenter->showedSaveFailed ); 75 | } 76 | 77 | public function testMappingIsPersisted(): void { 78 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() ); 79 | 80 | $useCase->saveMappings( 81 | 'Q2', 82 | [ 83 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ] 84 | ] 85 | ); 86 | 87 | $mappings = $this->repository->getMappings( new ItemId( 'Q2' ) ); 88 | 89 | $this->assertSame( 90 | self::VALID_PREDICATE, 91 | $mappings->asArray()[0]->predicate 92 | ); 93 | $this->assertSame( 94 | self::VALID_OBJECT, 95 | $mappings->asArray()[0]->object 96 | ); 97 | } 98 | 99 | public function testShouldShowInvalidMappingsWhenPredicateIsInvalid(): void { 100 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() ); 101 | 102 | $useCase->saveMappings( 103 | 'Q3', 104 | [ 105 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ], 106 | [ 'predicate' => self::INVALID_PREDICATE, 'object' => self::VALID_OBJECT ], 107 | ] 108 | ); 109 | 110 | $this->assertFalse( $this->presenter->showedSuccess ); 111 | $this->assertSame( 112 | self::INVALID_PREDICATE, 113 | $this->presenter->invalidMappings[0]['predicate'] 114 | ); 115 | $this->assertSame( 116 | self::VALID_OBJECT, 117 | $this->presenter->invalidMappings[0]['object'] 118 | ); 119 | $this->assertFalse( $this->presenter->showedSaveFailed ); 120 | } 121 | 122 | public function testShouldShowInvalidMappingsWhenObjectIsInvalid(): void { 123 | $useCase = new SaveMappingsUseCase( 124 | $this->presenter, 125 | $this->repository, 126 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ), 127 | new BasicEntityIdParser(), 128 | new MappingListSerializer(), 129 | $this->newSucceedingAuthorizer(), 130 | new FailingObjectValidator() 131 | ); 132 | 133 | $useCase->saveMappings( 134 | 'Q3', 135 | [ 136 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::INVALID_OBJECT ], 137 | ] 138 | ); 139 | 140 | $this->assertFalse( $this->presenter->showedSuccess ); 141 | $this->assertSame( 142 | self::VALID_PREDICATE, 143 | $this->presenter->invalidMappings[0]['predicate'] 144 | ); 145 | $this->assertSame( 146 | self::INVALID_OBJECT, 147 | $this->presenter->invalidMappings[0]['object'] 148 | ); 149 | $this->assertFalse( $this->presenter->showedSaveFailed ); 150 | } 151 | 152 | public function testShouldShowSaveFailed(): void { 153 | $useCase = new SaveMappingsUseCase( 154 | $this->presenter, 155 | new ThrowingMappingRepository(), 156 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ), 157 | new BasicEntityIdParser(), 158 | new MappingListSerializer(), 159 | $this->newSucceedingAuthorizer(), 160 | new SucceedingObjectValidator() 161 | ); 162 | 163 | $useCase->saveMappings( 164 | 'Q4', 165 | [ 166 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ] 167 | ] 168 | ); 169 | 170 | $this->assertFalse( $this->presenter->showedSuccess ); 171 | $this->assertSame( [], $this->presenter->invalidMappings ); 172 | $this->assertTrue( $this->presenter->showedSaveFailed ); 173 | } 174 | 175 | public function testShouldShowInvalidEntityId(): void { 176 | $useCase = new SaveMappingsUseCase( 177 | $this->presenter, 178 | new ThrowingMappingRepository(), 179 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ), 180 | new BasicEntityIdParser(), 181 | new MappingListSerializer(), 182 | $this->newSucceedingAuthorizer(), 183 | new SucceedingObjectValidator() 184 | ); 185 | 186 | $useCase->saveMappings( 187 | 'NotId', 188 | [ 189 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ] 190 | ] 191 | ); 192 | 193 | $this->assertTrue( $this->presenter->showedInvalidEntityId ); 194 | } 195 | 196 | public function testShouldShowPermissionDenied(): void { 197 | $useCase = new SaveMappingsUseCase( 198 | $this->presenter, 199 | new PermissionDeniedMappingRepository(), 200 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ), 201 | new BasicEntityIdParser(), 202 | new MappingListSerializer(), 203 | $this->newFailingAuthorizer(), 204 | new SucceedingObjectValidator() 205 | ); 206 | 207 | $useCase->saveMappings( 208 | 'Q1', 209 | [ 210 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ] 211 | ] 212 | ); 213 | 214 | $this->assertTrue( $this->presenter->showedPermissionDenied ); 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /tests/Application/WikibaseUrlObjectValidatorTest.php: -------------------------------------------------------------------------------- 1 | newObjectValidator(); 18 | } 19 | 20 | public function provideValidObjects(): iterable { 21 | yield 'Standard URL' => [ 'http://www.w3.org/2002/07/owl#sameAs' ]; 22 | yield 'Other protocol' => [ 'ftp://example.com' ]; 23 | yield 'Without TLD' => [ 'http://example' ]; 24 | yield 'Non-ASCII' => [ 'http://exåmple.com' ]; 25 | } 26 | 27 | /** 28 | * @dataProvider provideValidObjects 29 | */ 30 | public function testIsValid( string $object ): void { 31 | $validator = $this->createValidator(); 32 | 33 | $this->assertTrue( $validator->isValid( $object ) ); 34 | } 35 | 36 | public function provideInvalidObjects(): iterable { 37 | yield 'Missing protocol' => [ 'example.com' ]; 38 | yield 'Missing slash' => [ 'http:/example.com' ]; 39 | } 40 | 41 | /** 42 | * @dataProvider provideInvalidObjects 43 | */ 44 | public function testIsInvalid( string $object ): void { 45 | $validator = $this->createValidator(); 46 | 47 | $this->assertFalse( $validator->isValid( $object ) ); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /tests/DataAccess/CombiningMappingPredicatesLookupTest.php: -------------------------------------------------------------------------------- 1 | setAllowedPredicates( [] ); 20 | $this->createConfigPage( '' ); 21 | 22 | $lookup = WikibaseRdfExtension::getInstance()->newMappingPredicatesLookup(); 23 | 24 | $this->assertEquals( 25 | new PredicateList(), 26 | $lookup->getMappingPredicates() 27 | ); 28 | } 29 | 30 | public function testEmptyLocalSettingsAndValidPageConfigGetsCombined(): void { 31 | $this->setAllowedPredicates( [] ); 32 | $this->createConfigPage( "foo:bar\nbar:Baz" ); 33 | 34 | $lookup = WikibaseRdfExtension::getInstance()->newMappingPredicatesLookup(); 35 | 36 | $this->assertEquals( 37 | new PredicateList( [ 38 | new Predicate( 'foo:bar' ), 39 | new Predicate( 'bar:Baz' ) 40 | ] ), 41 | $lookup->getMappingPredicates() 42 | ); 43 | } 44 | 45 | public function testValidLocalSettingsAndEmptyPageConfigGetsCombined(): void { 46 | $this->setAllowedPredicates( [ 'owl:sameAs', 'owl:SymmetricProperty' ] ); 47 | $this->createConfigPage( '' ); 48 | 49 | $lookup = WikibaseRdfExtension::getInstance()->newMappingPredicatesLookup(); 50 | 51 | $this->assertEquals( 52 | new PredicateList( [ 53 | new Predicate( 'owl:sameAs' ), 54 | new Predicate( 'owl:SymmetricProperty' ), 55 | ] ), 56 | $lookup->getMappingPredicates() 57 | ); 58 | } 59 | 60 | public function testInvalidLocalSettingsAreIgnored(): void { 61 | $this->setAllowedPredicates( [ 'owl:sameAs', 'fooBar' ] ); 62 | $this->createConfigPage( '' ); 63 | 64 | $lookup = WikibaseRdfExtension::getInstance()->newMappingPredicatesLookup(); 65 | 66 | $this->assertEquals( 67 | new PredicateList( [ 68 | new Predicate( 'owl:sameAs' ), 69 | ] ), 70 | $lookup->getMappingPredicates() 71 | ); 72 | } 73 | 74 | public function testValidLocalSettingsAndValidPageConfigGetsCombined(): void { 75 | $this->setAllowedPredicates( [ 'owl:sameAs', 'owl:SymmetricProperty' ] ); 76 | $this->createConfigPage( "foo:bar\nbar:Baz" ); 77 | 78 | $lookup = WikibaseRdfExtension::getInstance()->newMappingPredicatesLookup(); 79 | 80 | $this->assertEquals( 81 | new PredicateList( [ 82 | new Predicate( 'owl:sameAs' ), 83 | new Predicate( 'owl:SymmetricProperty' ), 84 | new Predicate( 'foo:bar' ), 85 | new Predicate( 'bar:Baz' ), 86 | ] ), 87 | $lookup->getMappingPredicates() 88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /tests/DataAccess/LocalSettingsMappingPredicatesLookupTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 22 | new PredicateList(), 23 | $lookup->getMappingPredicates() 24 | ); 25 | } 26 | 27 | public function testValidPredicatesAreInList(): void { 28 | $mappings = [ 'foo:bar', 'bar:baz' ]; 29 | $lookup = new LocalSettingsMappingPredicatesLookup( $mappings ); 30 | 31 | $this->assertEquals( 32 | new PredicateList( [ new Predicate( 'foo:bar' ), new Predicate( 'bar:baz' ) ] ), 33 | $lookup->getMappingPredicates() 34 | ); 35 | } 36 | 37 | public function testInvalidPredicatesAreNotInList(): void { 38 | $mappings = [ 'foo:bar', 'not valid', 'bar:baz' ]; 39 | $lookup = new LocalSettingsMappingPredicatesLookup( $mappings ); 40 | 41 | $this->assertEquals( 42 | new PredicateList( [ new Predicate( 'foo:bar' ), new Predicate( 'bar:baz' ) ] ), 43 | $lookup->getMappingPredicates() 44 | ); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/DataAccess/PredicatesDeserializerTest.php: -------------------------------------------------------------------------------- 1 | newPredicateDeserializer()->deserialize( "foo:bar\nbar:Baz" ); 26 | 27 | $this->assertEquals( 28 | new PredicateList( [ 29 | new Predicate( 'foo:bar' ), 30 | new Predicate( 'bar:Baz' ), 31 | ] ), 32 | $predicates 33 | ); 34 | } 35 | 36 | public function testBlankLinesAreIgnored(): void { 37 | $predicates = $this->newPredicateDeserializer()->deserialize( "\n\nfoo:bar\n\n\nbar:Baz\n\n" ); 38 | 39 | $this->assertEquals( 40 | new PredicateList( [ 41 | new Predicate( 'foo:bar' ), 42 | new Predicate( 'bar:Baz' ), 43 | ] ), 44 | $predicates 45 | ); 46 | } 47 | 48 | public function testWhitespaceIsIgnored(): void { 49 | $predicates = $this->newPredicateDeserializer()->deserialize( " foo:bar \n \n bar:Baz " ); 50 | 51 | $this->assertEquals( 52 | new PredicateList( [ 53 | new Predicate( 'foo:bar' ), 54 | new Predicate( 'bar:Baz' ), 55 | ] ), 56 | $predicates 57 | ); 58 | } 59 | 60 | public function testInvalidPredicatesResultInEmptyList(): void { 61 | $predicates = $this->newPredicateDeserializer()->deserialize( "notValid\nfoo:bar\nbar:Baz\nalsoNotValid" ); 62 | 63 | $this->assertEquals( 64 | new PredicateList(), 65 | $predicates 66 | ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/DataAccess/PredicatesTextValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( $validator->validate( "foo:bar\nbar:Baz" ) ); 19 | 20 | $this->assertSame( 21 | [ 'foo:bar', 'bar:Baz' ], 22 | $validator->getValidPredicates() 23 | ); 24 | } 25 | 26 | public function testBlankLinesAreIgnored(): void { 27 | $validator = new PredicatesTextValidator(); 28 | 29 | $this->assertTrue( $validator->validate( "\n\nfoo:bar\n\n\nbar:Baz\n\n" ) ); 30 | 31 | $this->assertSame( 32 | [ 'foo:bar', 'bar:Baz' ], 33 | $validator->getValidPredicates() 34 | ); 35 | } 36 | 37 | public function testWhitespaceIsIgnored(): void { 38 | $validator = new PredicatesTextValidator(); 39 | 40 | $this->assertTrue( $validator->validate( " foo:bar \n \n bar:Baz " ) ); 41 | 42 | $this->assertSame( 43 | [ 'foo:bar', 'bar:Baz' ], 44 | $validator->getValidPredicates() 45 | ); 46 | } 47 | 48 | public function testInvalidPredicatesResultInEmptyList(): void { 49 | $validator = new PredicatesTextValidator(); 50 | 51 | $this->assertFalse( $validator->validate( "notValid\nfoo:bar\nbar:Baz\nalsoNotValid" ) ); 52 | 53 | $this->assertSame( 54 | [ 'notValid', 'alsoNotValid' ], 55 | $validator->getInvalidPredicates() 56 | ); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/DataAccess/WikiMappingPredicatesLookupTest.php: -------------------------------------------------------------------------------- 1 | getTitleParser(), 27 | MediaWikiServices::getInstance()->getRevisionLookup() 28 | ), 29 | new PredicatesDeserializer( 30 | new PredicatesTextValidator() 31 | ), 32 | WikibaseRdfExtension::CONFIG_PAGE_TITLE 33 | ); 34 | } 35 | 36 | public function testEmptyConfigResultsInEmptyList(): void { 37 | $this->createConfigPage( '' ); 38 | 39 | $lookup = $this->newWikiMappingPredicatesLookup(); 40 | 41 | $this->assertEquals( 42 | new PredicateList(), 43 | $lookup->getMappingPredicates() 44 | ); 45 | } 46 | 47 | public function testValidConfigIsRetrieved(): void { 48 | $this->createConfigPage( "foo:bar\nbar:Baz" ); 49 | 50 | $lookup = $this->newWikiMappingPredicatesLookup(); 51 | 52 | $this->assertEquals( 53 | new PredicateList( [ 54 | new Predicate( 'foo:bar' ), 55 | new Predicate( 'bar:Baz' ) 56 | ] ), 57 | $lookup->getMappingPredicates() 58 | ); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/EntryPoints/MappingUiLoadingTest.php: -------------------------------------------------------------------------------- 1 | createItemWithMappings( 19 | new ItemId( 'Q90019001' ), 20 | new MappingList( [ 21 | new Mapping( 'wiki:hosting', 'https://pro.wiki' ) 22 | ] ) 23 | ); 24 | 25 | $html = $this->getPageHtml( 'Item:Q90019001' ); 26 | $this->assertStringContainsString( 'wikibase-rdf', $html ); 27 | $this->assertStringContainsString( 'wiki:hosting', $html ); 28 | $this->assertStringContainsString( 'https://pro.wiki', $html ); 29 | } 30 | 31 | public function test404ItemHasNoEditingUI(): void { 32 | $html = $this->getPageHtml( 'Item:Q404404404' ); 33 | $this->assertStringNotContainsString( 'wikibase-rdf', $html ); 34 | } 35 | 36 | public function testExistingNonItemHasNoEditingUi(): void { 37 | $this->editPage( 'MappingUiLoadingTest', 'Pink Fluffy Unicorns Dancing On Rainbows' ); 38 | $html = $this->getPageHtml( 'MappingUiLoadingTest' ); 39 | $this->assertStringContainsString( 'Pink Fluffy Unicorns Dancing On Rainbows', $html ); 40 | $this->assertStringNotContainsString( 'wikibase-rdf', $html ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/EntryPoints/Rest/GetMappingsApiTest.php: -------------------------------------------------------------------------------- 1 | setAllowedPredicates( [ 'owl:sameAs' ] ); 27 | 28 | $this->createItem( new ItemId( 'Q1' ) ); 29 | } 30 | 31 | public function testHappyPathWithNoMappings(): void { 32 | $response = $this->executeHandler( 33 | WikibaseRdfExtension::getMappingsApiFactory(), 34 | new RequestData( [ 'pathParams' => [ 'entity_id' => 'Q1' ] ] ) 35 | ); 36 | 37 | $this->assertSame( 200, $response->getStatusCode() ); 38 | $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) ); 39 | 40 | $data = json_decode( $response->getBody()->getContents(), true ); 41 | $this->assertIsArray( $data ); 42 | 43 | $this->assertArrayHasKey( 'mappings', $data ); 44 | $this->assertSame( [], $data['mappings'] ); 45 | } 46 | 47 | public function testHappyPathWithExistingMappings(): void { 48 | $this->createItem( new ItemId( 'Q2' ) ); 49 | $this->setMappings( 50 | new ItemId( 'Q2' ), 51 | new MappingList( [ 52 | new Mapping( 'owl:sameAs', 'http://example.com' ) 53 | ] ) 54 | ); 55 | 56 | $response = $this->executeHandler( 57 | WikibaseRdfExtension::getMappingsApiFactory(), 58 | new RequestData( [ 'pathParams' => [ 'entity_id' => 'Q2' ] ] ) 59 | ); 60 | 61 | $this->assertSame( 200, $response->getStatusCode() ); 62 | $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) ); 63 | 64 | $data = json_decode( $response->getBody()->getContents(), true ); 65 | $this->assertIsArray( $data ); 66 | 67 | $this->assertArrayHasKey( 'mappings', $data ); 68 | $this->assertSame( 69 | [ 70 | [ 'predicate' => 'owl:sameAs', 'object' => 'http://example.com' ] 71 | ], 72 | $data['mappings'] 73 | ); 74 | } 75 | 76 | public function testInvalidEntityId(): void { 77 | $response = $this->executeHandler( 78 | WikibaseRdfExtension::getMappingsApiFactory(), 79 | new RequestData( [ 'pathParams' => [ 'entity_id' => 'NotId' ] ] ) 80 | ); 81 | 82 | $this->assertSame( 400, $response->getStatusCode() ); 83 | $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) ); 84 | 85 | $data = json_decode( $response->getBody()->getContents(), true ); 86 | $this->assertIsArray( $data ); 87 | 88 | $this->assertStringContainsString( 89 | 'wikibase-rdf-entity-id-invalid', 90 | $data['messageTranslations']['qqx'] 91 | ); 92 | } 93 | 94 | public function testMissingEntityId(): void { 95 | $response = $this->executeHandler( 96 | WikibaseRdfExtension::getMappingsApiFactory(), 97 | new RequestData( [ 'pathParams' => [ 'entity_id' => 'Q1000000000' ] ] ) 98 | ); 99 | 100 | $this->assertSame( 200, $response->getStatusCode() ); 101 | $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) ); 102 | 103 | $data = json_decode( $response->getBody()->getContents(), true ); 104 | $this->assertIsArray( $data ); 105 | 106 | $this->assertArrayHasKey( 'mappings', $data ); 107 | $this->assertSame( [], $data['mappings'] ); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /tests/MappingListSerializerTest.php: -------------------------------------------------------------------------------- 1 | mappingListFromArray( [ 22 | [ 'predicate' => 'foo:Bar', 'object' => 'https://example.com' ], 23 | [ 'predicate' => 'bar:Baz', 'object' => 'https://test.com' ], 24 | ] ); 25 | 26 | $this->assertEquals( 27 | new MappingList( [ 28 | new Mapping( 'foo:Bar', 'https://example.com' ), 29 | new Mapping( 'bar:Baz', 'https://test.com' ) 30 | ] ), 31 | $mappings 32 | ); 33 | } 34 | 35 | public function testMappingListFromArrayWithInvalidMappingsThrowsException(): void { 36 | $serializer = new MappingListSerializer(); 37 | 38 | $this->expectException( InvalidArgumentException::class ); 39 | 40 | $serializer->mappingListFromArray( [ 41 | [ 'predicate' => 'foo:Bar', 'object' => 'https://example.com' ], 42 | [ 'predicate' => 'bar:Baz', 'object' => 'not:valid' ], 43 | ] ); 44 | } 45 | 46 | public function testValidMappingListFromArrayWithValidMappings(): void { 47 | $serializer = new MappingListSerializer(); 48 | 49 | $mappings = $serializer->validMappingListFromArray( [ 50 | [ 'predicate' => 'foo:Bar', 'object' => 'https://example.com' ], 51 | [ 'predicate' => 'bar:Baz', 'object' => 'https://test.com' ], 52 | ] ); 53 | 54 | $this->assertEquals( 55 | new MappingList( [ 56 | new Mapping( 'foo:Bar', 'https://example.com' ), 57 | new Mapping( 'bar:Baz', 'https://test.com' ) 58 | ] ), 59 | $mappings 60 | ); 61 | } 62 | 63 | public function testValidMappingListFromArrayWithInvalidMappingsDoesNotThrowException(): void { 64 | $serializer = new MappingListSerializer(); 65 | 66 | $mappings = $serializer->validMappingListFromArray( [ 67 | [ 'predicate' => 'foo:Bar', 'object' => 'https://example.com' ], 68 | [ 'predicate' => 'bar:Baz', 'object' => 'not:valid' ], 69 | ] ); 70 | 71 | $this->assertEquals( 72 | new MappingList( [ 73 | new Mapping( 'foo:Bar', 'https://example.com' ), 74 | ] ), 75 | $mappings 76 | ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tests/Persistence/ContentSlotMappingRepositoryTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 23 | new MappingList(), 24 | $this->newRepo()->getMappings( new ItemId( 'Q404' ) ) 25 | ); 26 | } 27 | 28 | private function newRepo(): ContentSlotMappingRepository { 29 | return new ContentSlotMappingRepository( 30 | contentRepository: new InMemoryEntityContentRepository(), 31 | serializer: new MappingListSerializer() 32 | ); 33 | } 34 | 35 | public function testPersistenceRoundTrip(): void { 36 | $repo = $this->newRepo(); 37 | 38 | $repo->setMappings( 39 | new ItemId( 'Q1' ), 40 | new MappingList( [ 41 | new Mapping( 'owl:q1-predicate-1', 'http://q1-object-1' ), 42 | ] ) 43 | ); 44 | 45 | $repo->setMappings( 46 | new ItemId( 'Q2' ), 47 | new MappingList( [ 48 | new Mapping( 'owl:q2-predicate-1', 'http://q2-object-1' ), 49 | new Mapping( 'owl:q2-predicate-2', 'http://q2-object-2' ), 50 | ] ) 51 | ); 52 | 53 | $repo->setMappings( 54 | new ItemId( 'Q3' ), 55 | new MappingList( [ 56 | new Mapping( 'owl:q3-predicate-1', 'http://q3-object-1' ), 57 | ] ) 58 | ); 59 | 60 | $this->assertEquals( 61 | new MappingList( [ 62 | new Mapping( 'owl:q2-predicate-1', 'http://q2-object-1' ), 63 | new Mapping( 'owl:q2-predicate-2', 'http://q2-object-2' ), 64 | ] ), 65 | $repo->getMappings( new ItemId( 'Q2' ) ) 66 | ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/Persistence/InMemoryMappingRepositoryUnitTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 22 | new MappingList(), 23 | $repo->getMappings( new ItemId( 'Q2' ) ) 24 | ); 25 | } 26 | 27 | public function testPersistenceRoundTrip(): void { 28 | $repo = new InMemoryMappingRepository(); 29 | 30 | $repo->setMappings( 31 | new ItemId( 'Q1' ), 32 | new MappingList( [ 33 | new Mapping( 'owl:sameAs', 'https://example.com/uri/1' ) 34 | ] ) 35 | ); 36 | 37 | $repo->setMappings( 38 | new ItemId( 'Q2' ), 39 | new MappingList( [ 40 | new Mapping( 'owl:sameAs', 'https://example.com/uri/2' ) 41 | ] ) 42 | ); 43 | 44 | $repo->setMappings( 45 | new ItemId( 'Q3' ), 46 | new MappingList( [ 47 | new Mapping( 'owl:sameAs', 'https://example.com/uri/3' ) 48 | ] ) 49 | ); 50 | 51 | $this->assertEquals( 52 | new MappingList( [ 53 | new Mapping( 'owl:sameAs', 'https://example.com/uri/2' ) 54 | ] ), 55 | $repo->getMappings( new ItemId( 'Q2' ) ) 56 | ); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/Persistence/SlotEntityContentRepositoryTest.php: -------------------------------------------------------------------------------- 1 | createItem( new ItemId( 'Q100' ) ); 27 | } 28 | 29 | private function newRepo(): SlotEntityContentRepository { 30 | return WikibaseRdfExtension::getInstance()->newEntityContentRepository( 31 | self::getTestUser()->getUser() 32 | ); 33 | } 34 | 35 | public function testReturnsNullWhenEntityNotFound(): void { 36 | $this->assertNull( 37 | $this->newRepo()->getContent( new ItemId( 'Q404' ) ) 38 | ); 39 | } 40 | 41 | public function testReturnsNullWhenSlotNotFound(): void { 42 | $this->assertNull( 43 | $this->newRepo()->getContent( new ItemId( 'Q100' ) ) 44 | ); 45 | } 46 | 47 | public function testCanSetContentWhenSlotDoesNotExistYet(): void { 48 | $repo = $this->newRepo(); 49 | 50 | $repo->setContent( 51 | new ItemId( 'Q100' ), 52 | new JsonContent( '{ "foo": 42 }' ) 53 | ); 54 | 55 | $this->assertEquals( 56 | new JsonContent( 57 | '{ 58 | "foo": 42 59 | }' 60 | ), 61 | $repo->getContent( new ItemId( 'Q100' ) ) 62 | ); 63 | } 64 | 65 | public function testSettingSlotForNonExistingPageResultsInException(): void { 66 | $this->expectException( Exception::class ); 67 | 68 | $this->newRepo()->setContent( 69 | new ItemId( 'Q404' ), 70 | new JsonContent( '{ "foo": 42 }' ) 71 | ); 72 | } 73 | 74 | public function testSetContentForExistingSlotOverridesPreviousValues(): void { 75 | $repo = $this->newRepo(); 76 | 77 | $repo->setContent( 78 | new ItemId( 'Q100' ), 79 | new JsonContent( '{ "foo": 42, "bar": 9001, "baz": 1337 }' ) 80 | ); 81 | 82 | $repo->setContent( 83 | new ItemId( 'Q100' ), 84 | new JsonContent( '{ "foo": 1, "bah": 2 }' ) 85 | ); 86 | 87 | $this->assertEquals( 88 | new JsonContent( 89 | '{ 90 | "foo": 1, 91 | "bah": 2 92 | }' 93 | ), 94 | $repo->getContent( new ItemId( 'Q100' ) ) 95 | ); 96 | } 97 | 98 | private function getRevisionLookup(): RevisionLookup { 99 | return MediaWikiServices::getInstance()->getRevisionLookup(); 100 | } 101 | 102 | public function testCanRetrieveRevision(): void { 103 | $repo = $this->newRepo(); 104 | 105 | $repo->setContent( 106 | new ItemId( 'Q100' ), 107 | new JsonContent( '{ "foo": 42 }' ) 108 | ); 109 | $firstRevisionId = $this->getRevisionLookup()->getRevisionByTitle( Title::newFromText( 'Item:Q100' ) )->getId(); 110 | 111 | $repo->setContent( 112 | new ItemId( 'Q100' ), 113 | new JsonContent( '{ "foo": 84 }' ) 114 | ); 115 | $latestRevisionId = $this->getRevisionLookup()->getRevisionByTitle( Title::newFromText( 'Item:Q100' ) )->getId(); 116 | 117 | $this->assertEquals( 118 | new JsonContent( 119 | '{ 120 | "foo": 42 121 | }' 122 | ), 123 | $repo->getContent( new ItemId( 'Q100' ), $firstRevisionId ) 124 | ); 125 | 126 | $this->assertEquals( 127 | new JsonContent( 128 | '{ 129 | "foo": 84 130 | }' 131 | ), 132 | $repo->getContent( new ItemId( 'Q100' ), $latestRevisionId ) 133 | ); 134 | 135 | $this->assertEquals( 136 | new JsonContent( 137 | '{ 138 | "foo": 84 139 | }' 140 | ), 141 | $repo->getContent( new ItemId( 'Q100' ) ) 142 | ); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /tests/Persistence/SqlAllMappingsLookupPerformanceTest.php: -------------------------------------------------------------------------------- 1 | setAllowedPredicates( [ 'foo:bar' ] ); 28 | } 29 | 30 | private function newSqlAllMappingsLookup(): SqlAllMappingsLookup { 31 | return new SqlAllMappingsLookup( 32 | $this->getDb(), 33 | WikibaseRdfExtension::SLOT_NAME, 34 | WikibaseRepo::getEntityIdParser(), 35 | WikibaseRdfExtension::getInstance()->newMappingListSerializer() 36 | ); 37 | } 38 | 39 | public function providePerformance(): iterable { 40 | // Items, Mappings, Revisions, Expected Milliseconds 41 | yield [ 1, 1, 1, 1 ]; 42 | yield [ 500, 1, 1, 10 ]; 43 | yield [ 1, 500, 1, 5 ]; 44 | yield [ 1, 1, 500, 1 ]; 45 | yield [ 50, 50, 50, 15 ]; 46 | yield [ 500, 100, 1, 100 ]; 47 | } 48 | 49 | /** 50 | * @dataProvider providePerformance 51 | */ 52 | public function testPerformance( int $items, int $mappings, int $revisions, int $expectedTime ): void { 53 | print( "\n$items items with $mappings mappings and $revisions revisions\n" ); 54 | 55 | $setupStart = hrtime( true ); 56 | for ( $i = 1; $i <= $items; $i++ ) { 57 | $id = new ItemId( "Q$i" ); 58 | $this->createItem( $id ); 59 | $this->setMappingsRepeatedly( $id, $mappings, $revisions ); 60 | } 61 | 62 | $lookup = $this->newSqlAllMappingsLookup(); 63 | 64 | $setupEnd = hrtime( true ); 65 | $setupTook = ( $setupEnd - $setupStart ) / 1000000000; 66 | print( "Setup: {$setupTook}s\n" ); 67 | 68 | $start = hrtime( true ); 69 | $allMappings = $lookup->getAllMappings(); 70 | $end = hrtime( true ); 71 | $took = ( $end - $start ) / 1000000; 72 | 73 | print( "Get all mappings: {$took}ms\n" ); 74 | 75 | $this->assertSame( 76 | $items * $mappings, 77 | array_sum( 78 | array_map( fn ( MappingListAndId $mapping ) => count( $mapping->mappingList->asArray() ), $allMappings ) 79 | ) 80 | ); 81 | $this->assertLessThan( $expectedTime, $took ); 82 | } 83 | 84 | private function createBulkMappings( int $count, int $revision ): MappingList { 85 | return new MappingList( 86 | array_map( 87 | fn ( $i ) => new Mapping( 'foo:bar', "http://example.com/$i-$revision" ), 88 | range( 0, $count - 1 ) 89 | ) 90 | ); 91 | } 92 | 93 | private function setMappingsRepeatedly( EntityId $entityId, int $mappings, int $revisions ): void { 94 | for ( $j = 0; $j < $revisions; $j++ ) { 95 | $mappingList = $this->createBulkMappings( $mappings, $j ); 96 | $this->setMappings( $entityId, $mappingList ); 97 | } 98 | } 99 | 100 | protected function setMappings( EntityId $entityId, MappingList $mappingList ): void { 101 | $user = self::getTestSysop()->getUser(); 102 | WikibaseRdfExtension::getInstance()->newMappingRepository( $user )->setMappings( $entityId, $mappingList ); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /tests/Persistence/SqlAllMappingsLookupTest.php: -------------------------------------------------------------------------------- 1 | setAllowedPredicates( [ 'foo:foo', 'foo:bar', 'foo:baz' ] ); 27 | } 28 | 29 | private function newSqlAllMappingsLookup(): SqlAllMappingsLookup { 30 | return new SqlAllMappingsLookup( 31 | $this->getDb(), 32 | WikibaseRdfExtension::SLOT_NAME, 33 | WikibaseRepo::getEntityIdParser(), 34 | WikibaseRdfExtension::getInstance()->newMappingListSerializer() 35 | ); 36 | } 37 | 38 | private function saveTestMappings(): void { 39 | $this->createItem( new ItemId( 'Q99001' ) ); 40 | $this->createItemWithMappings( 41 | new ItemId( 'Q99002' ), 42 | new MappingList( [ 43 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ) 44 | ] ) 45 | ); 46 | $this->createItemWithMappings( 47 | new ItemId( 'Q99003' ), 48 | new MappingList( [ 49 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 50 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 51 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 52 | ] ) 53 | ); 54 | $this->createProperty( new NumericPropertyId( 'P99001' ) ); 55 | $this->createPropertyWithMappings( 56 | new NumericPropertyId( 'P99002' ), 57 | new MappingList( [ 58 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ) 59 | ] ) 60 | ); 61 | $this->createPropertyWithMappings( 62 | new NumericPropertyId( 'P99003' ), 63 | new MappingList( [ 64 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 65 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 66 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 67 | ] ) 68 | ); 69 | } 70 | 71 | private function getTestMappingsList(): array { 72 | return [ 73 | new MappingListAndId( 74 | new ItemId( 'Q99002' ), 75 | new MappingList( [ 76 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ) 77 | ] ) 78 | ), 79 | new MappingListAndId( 80 | new ItemId( 'Q99003' ), 81 | new MappingList( [ 82 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 83 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 84 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 85 | ] ) 86 | ), 87 | new MappingListAndId( 88 | new NumericPropertyId( 'P99002' ), 89 | new MappingList( [ 90 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ) 91 | ] ) 92 | ), 93 | new MappingListAndId( 94 | new NumericPropertyId( 'P99003' ), 95 | new MappingList( [ 96 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 97 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 98 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 99 | ] ) 100 | ) 101 | ]; 102 | } 103 | 104 | public function testReturnsAllMappings(): void { 105 | $this->saveTestMappings(); 106 | 107 | $this->assertEquals( 108 | $this->getTestMappingsList(), 109 | $this->newSqlAllMappingsLookup()->getAllMappings() 110 | ); 111 | } 112 | 113 | public function testEmptyMappings(): void { 114 | $this->assertSame( 115 | [], 116 | $this->newSqlAllMappingsLookup()->getAllMappings() 117 | ); 118 | } 119 | 120 | public function testReturnsLatestRevisionsWhenMappingSlotsAreModified(): void { 121 | $this->saveTestMappings(); 122 | 123 | $initialCount = count( $this->newSqlAllMappingsLookup()->getAllMappings() ); 124 | 125 | $this->setMappings( 126 | new ItemId( 'Q99002' ), 127 | new MappingList( [ 128 | new Mapping( 'foo:foo', 'https://example.com/#foo2' ), 129 | new Mapping( 'foo:baz', 'https://example.com/#baz2' ) 130 | ] ) 131 | ); 132 | $this->setMappings( 133 | new NumericPropertyId( 'P99002' ), 134 | new MappingList( [ 135 | new Mapping( 'foo:foo', 'https://example.com/#foo2' ), 136 | new Mapping( 'foo:baz', 'https://example.com/#baz2' ) 137 | ] ) 138 | ); 139 | 140 | $allMappings = $this->newSqlAllMappingsLookup()->getAllMappings(); 141 | 142 | $this->assertCount( $initialCount, $allMappings ); 143 | 144 | $this->assertEquals( 145 | [ 146 | new MappingListAndId( 147 | new ItemId( 'Q99002' ), 148 | new MappingList( [ 149 | new Mapping( 'foo:foo', 'https://example.com/#foo2' ), 150 | new Mapping( 'foo:baz', 'https://example.com/#baz2' ) 151 | ] ) 152 | ), 153 | new MappingListAndId( 154 | new ItemId( 'Q99003' ), 155 | new MappingList( [ 156 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 157 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 158 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 159 | ] ) 160 | ), 161 | new MappingListAndId( 162 | new NumericPropertyId( 'P99002' ), 163 | new MappingList( [ 164 | new Mapping( 'foo:foo', 'https://example.com/#foo2' ), 165 | new Mapping( 'foo:baz', 'https://example.com/#baz2' ) 166 | ] ) 167 | ), 168 | new MappingListAndId( 169 | new NumericPropertyId( 'P99003' ), 170 | new MappingList( [ 171 | new Mapping( 'foo:foo', 'https://example.com/#foo1' ), 172 | new Mapping( 'foo:bar', 'https://example.com/#bar1' ), 173 | new Mapping( 'foo:baz', 'https://example.com/#baz1' ) 174 | ] ) 175 | ) 176 | ], 177 | $allMappings 178 | ); 179 | } 180 | 181 | public function testReturnsLatestRevisionsWhenMainSlotIsModified(): void { 182 | $this->saveTestMappings(); 183 | $this->modifyItem( new ItemId( 'Q99002' ), 'NewText' ); 184 | 185 | $this->assertEquals( 186 | $this->getTestMappingsList(), 187 | $this->newSqlAllMappingsLookup()->getAllMappings() 188 | ); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /tests/Persistence/UserBasedEntityMappingsAuthorizerTest.php: -------------------------------------------------------------------------------- 1 | createItem( new ItemId( 'Q1245' ) ); 22 | } 23 | 24 | private function newAuthorizer( User $user ): UserBasedEntityMappingsAuthorizer { 25 | return new UserBasedEntityMappingsAuthorizer( 26 | $user, 27 | WikibaseRdfExtension::getInstance()->newEntityPermissionChecker( $this->getServiceContainer() ) 28 | ); 29 | } 30 | 31 | public function testAnonymousUserCanEdit(): void { 32 | $this->setMwGlobals( 33 | 'wgGroupPermissions', 34 | [ 35 | '*' => [ 'read' => true, 'edit' => true ], 36 | ] 37 | ); 38 | 39 | $authorizer = $this->newAuthorizer( $this->getTestUser()->getUser() ); 40 | 41 | $this->assertTrue( $authorizer->canEditEntityMappings( new ItemId( 'Q1245' ) ) ); 42 | } 43 | 44 | public function testAnonymousUserCannotEdit(): void { 45 | $this->setMwGlobals( 46 | 'wgGroupPermissions', 47 | [ 48 | '*' => [ 'read' => true, 'edit' => false ], 49 | ] 50 | ); 51 | 52 | $authorizer = $this->newAuthorizer( $this->getTestUser()->getUser() ); 53 | 54 | $this->assertFalse( $authorizer->canEditEntityMappings( new ItemId( 'Q1245' ) ) ); 55 | } 56 | 57 | public function testUserWithGroupPermissionCanEdit(): void { 58 | $this->setMwGlobals( 59 | 'wgGroupPermissions', 60 | [ 61 | '*' => [ 'read' => true, 'edit' => false ], 62 | 'testGroup' => [ 'read' => true, 'edit' => true ], 63 | ] 64 | ); 65 | 66 | $authorizer = $this->newAuthorizer( $this->getTestUser( [ 'testGroup' ] )->getUser() ); 67 | 68 | $this->assertTrue( $authorizer->canEditEntityMappings( new ItemId( 'Q1245' ) ) ); 69 | } 70 | 71 | public function testUserWithoutGroupPermissionCannotEdit(): void { 72 | $this->setMwGlobals( 73 | 'wgGroupPermissions', 74 | [ 75 | '*' => [ 'read' => true, 'edit' => false ], 76 | 'testGroup' => [ 'read' => true, 'edit' => false ], 77 | ] 78 | ); 79 | 80 | $authorizer = $this->newAuthorizer( $this->getTestUser( [ 'testGroup' ] )->getUser() ); 81 | 82 | $this->assertFalse( $authorizer->canEditEntityMappings( new ItemId( 'Q1245' ) ) ); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /tests/Presentation/HtmlMappingsPresenterTest.php: -------------------------------------------------------------------------------- 1 | getAllowedPredicates(), false ); 34 | $mapping1 = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 35 | $mapping2 = new Mapping( 'skos:exactMatch', 'http://www.example.com/foo' ); 36 | 37 | $presenter->showMappings( 38 | new MappingList( [ $mapping1, $mapping2 ] ), 39 | true 40 | ); 41 | $html = $presenter->getHtml(); 42 | 43 | $this->assertStringContainsString( $mapping1->predicate, $html ); 44 | $this->assertStringContainsString( $mapping1->object, $html ); 45 | $this->assertStringContainsString( $mapping2->predicate, $html ); 46 | $this->assertStringContainsString( $mapping2->object, $html ); 47 | } 48 | 49 | public function testMappingUrlIsALink(): void { 50 | $presenter = new HtmlMappingsPresenter( $this->getAllowedPredicates(), false ); 51 | $mapping = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 52 | 53 | $presenter->showMappings( 54 | new MappingList( [ $mapping ] ), 55 | true 56 | ); 57 | $html = $presenter->getHtml(); 58 | 59 | $this->assertStringContainsString( 'getAllowedPredicates(), false ); 64 | $mapping1 = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 65 | $mapping2 = new Mapping( 'skos:exactMatch', 'http://www.example.com/foo' ); 66 | 67 | $presenter->showMappings( 68 | new MappingList( [ $mapping1, $mapping2 ] ), 69 | true 70 | ); 71 | $html = $presenter->getHtml(); 72 | 73 | $this->assertStringContainsString( self::CLASS_ACTION_EDIT, $html ); 74 | $this->assertStringContainsString( self::CLASS_ACTION_SAVE, $html ); 75 | $this->assertStringContainsString( self::CLASS_ACTION_REMOVE, $html ); 76 | $this->assertStringContainsString( self::CLASS_ACTION_CANCEL, $html ); 77 | $this->assertStringContainsString( self::CLASS_ACTION_ADD, $html ); 78 | } 79 | 80 | public function testEditActionsAreNotDisplayedWhenUserCannotEdit(): void { 81 | $presenter = new HtmlMappingsPresenter( $this->getAllowedPredicates(), false ); 82 | $mapping1 = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 83 | $mapping2 = new Mapping( 'skos:exactMatch', 'http://www.example.com/foo' ); 84 | 85 | $presenter->showMappings( 86 | new MappingList( [ $mapping1, $mapping2 ] ), 87 | false 88 | ); 89 | $html = $presenter->getHtml(); 90 | 91 | $this->assertStringNotContainsString( self::CLASS_ACTION_EDIT, $html ); 92 | $this->assertStringNotContainsString( self::CLASS_ACTION_SAVE, $html ); 93 | $this->assertStringNotContainsString( self::CLASS_ACTION_REMOVE, $html ); 94 | $this->assertStringNotContainsString( self::CLASS_ACTION_CANCEL, $html ); 95 | $this->assertStringNotContainsString( self::CLASS_ACTION_ADD, $html ); 96 | } 97 | 98 | public function testEditActionsAreNotDisplayedOnDiffPage(): void { 99 | $presenter = new HtmlMappingsPresenter( $this->getAllowedPredicates(), true ); 100 | $mapping1 = new Mapping( 'owl:sameAs', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' ); 101 | $mapping2 = new Mapping( 'skos:exactMatch', 'http://www.example.com/foo' ); 102 | 103 | $presenter->showMappings( 104 | new MappingList( [ $mapping1, $mapping2 ] ), 105 | true 106 | ); 107 | $html = $presenter->getHtml(); 108 | 109 | $this->assertStringNotContainsString( self::CLASS_ACTION_EDIT, $html ); 110 | $this->assertStringNotContainsString( self::CLASS_ACTION_SAVE, $html ); 111 | $this->assertStringNotContainsString( self::CLASS_ACTION_REMOVE, $html ); 112 | $this->assertStringNotContainsString( self::CLASS_ACTION_CANCEL, $html ); 113 | $this->assertStringNotContainsString( self::CLASS_ACTION_ADD, $html ); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /tests/Presentation/HtmlPredicatesPresenterTest.php: -------------------------------------------------------------------------------- 1 | * Defined in LocalSettings.php'; 18 | 19 | public function testHtmlContainsOnlyLocalSettingsPredicates(): void { 20 | $presenter = new HtmlPredicatesPresenter(); 21 | $presenter->presentPredicates( 22 | new PredicateList( [ new Predicate( 'foo:bar' ), new Predicate( 'bar:baz' ) ] ), 23 | new PredicateList() 24 | ); 25 | 26 | $html = $presenter->getHtml(); 27 | 28 | $this->assertStringContainsString( '
  • foo:bar *
  • ', $html ); 29 | $this->assertStringContainsString( '
  • bar:baz *
  • ', $html ); 30 | $this->assertStringContainsString( self::LOCAL_SETTINGS_MESSAGE, $html ); 31 | } 32 | 33 | public function testHtmlContainsOnlyWikiConfigPredicates(): void { 34 | $presenter = new HtmlPredicatesPresenter(); 35 | $presenter->presentPredicates( 36 | new PredicateList(), 37 | new PredicateList( [ new Predicate( 'foo:bar' ), new Predicate( 'bar:baz' ) ] ) 38 | ); 39 | 40 | $html = $presenter->getHtml(); 41 | 42 | $this->assertStringContainsString( '
  • foo:bar
  • ', $html ); 43 | $this->assertStringContainsString( '
  • bar:baz
  • ', $html ); 44 | $this->assertStringNotContainsString( self::LOCAL_SETTINGS_MESSAGE, $html ); 45 | } 46 | 47 | public function testHtmlContainsLocalSettingsAndWikiConfigPredicates(): void { 48 | $presenter = new HtmlPredicatesPresenter(); 49 | $presenter->presentPredicates( 50 | new PredicateList( [ new Predicate( 'foo:bar' ) ] ), 51 | new PredicateList( [ new Predicate( 'bar:baz' ) ] ) 52 | ); 53 | 54 | $html = $presenter->getHtml(); 55 | 56 | $this->assertStringContainsString( '
  • foo:bar *
  • ', $html ); 57 | $this->assertStringContainsString( '
  • bar:baz
  • ', $html ); 58 | $this->assertStringContainsString( self::LOCAL_SETTINGS_MESSAGE, $html ); 59 | } 60 | 61 | public function testHtmlContainsMessageIfNoPredicatesAreConfigured(): void { 62 | $presenter = new HtmlPredicatesPresenter(); 63 | $presenter->presentPredicates( new PredicateList(), new PredicateList() ); 64 | 65 | $html = $presenter->getHtml(); 66 | 67 | $this->assertStringContainsString( 68 | 'No predicates are defined.', 69 | $html 70 | ); 71 | $this->assertStringNotContainsString( self::LOCAL_SETTINGS_MESSAGE, $html ); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /tests/Presentation/RDF/PropertyMappingPrefixBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 'wdt', $builder->getPrefix() ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tests/TestDoubles/FailingEntityMappingsAuthorizer.php: -------------------------------------------------------------------------------- 1 | */ 14 | public array $invalidMappings = []; 15 | public bool $showedSaveFailed = false; 16 | public bool $showedInvalidEntityId = false; 17 | public bool $showedPermissionDenied = false; 18 | 19 | public function presentSuccess(): void { 20 | $this->showedSuccess = true; 21 | } 22 | 23 | /** 24 | * @param array $mappings 25 | */ 26 | public function presentInvalidMappings( array $mappings ): void { 27 | $this->invalidMappings = $mappings; 28 | } 29 | 30 | public function presentSaveFailed(): void { 31 | $this->showedSaveFailed = true; 32 | } 33 | 34 | public function presentInvalidEntityId(): void { 35 | $this->showedInvalidEntityId = true; 36 | } 37 | 38 | public function presentPermissionDenied(): void { 39 | $this->showedPermissionDenied = true; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/TestDoubles/SucceedingEntityMappingsAuthorizer.php: -------------------------------------------------------------------------------- 1 | getContext()->getOutput()->setTitle( $title ); 30 | 31 | $article->view(); 32 | 33 | return $article->getContext()->getOutput()->getHTML(); 34 | } 35 | 36 | protected function createItemWithMappings( ItemId $itemId, MappingList $mappingList ): void { 37 | $this->createItem( $itemId ); 38 | $this->setMappings( $itemId, $mappingList ); 39 | } 40 | 41 | protected function createItem( ItemId $itemId ): void { 42 | $this->saveItem( new Item( $itemId ) ); 43 | } 44 | 45 | private function saveItem( Item $item ): void { 46 | WikibaseRepo::getEntityStore()->saveEntity( 47 | $item, 48 | '', 49 | self::getTestSysop()->getUser() 50 | ); 51 | } 52 | 53 | protected function createProperty( PropertyId $propertyId ): void { 54 | WikibaseRepo::getEntityStore()->saveEntity( 55 | new Property( $propertyId, null, 'testType' ), 56 | '', 57 | self::getTestSysop()->getUser() 58 | ); 59 | } 60 | 61 | protected function createPropertyWithMappings( PropertyId $propertyId, MappingList $mappingList ): void { 62 | $this->createProperty( $propertyId ); 63 | $this->setMappings( $propertyId, $mappingList ); 64 | } 65 | 66 | protected function setMappings( EntityId $entityId, MappingList $mappingList ): void { 67 | $user = self::getTestSysop()->getUser(); 68 | WikibaseRdfExtension::getInstance()->newMappingRepository( $user )->setMappings( $entityId, $mappingList ); 69 | } 70 | 71 | protected function modifyItem( ItemId $itemId, string $labelText ): void { 72 | $item = new Item( $itemId ); 73 | $item->setLabel( 'en', $labelText ); 74 | 75 | $this->saveItem( $item ); 76 | } 77 | 78 | /** 79 | * @param string[] $predicates 80 | */ 81 | protected function setAllowedPredicates( array $predicates ): void { 82 | $this->setMwGlobals( 'wgWikibaseRdfPredicates', $predicates ); 83 | } 84 | 85 | protected function createConfigPage( string $config ): void { 86 | $this->insertPage( 87 | 'MediaWiki:' . WikibaseRdfExtension::CONFIG_PAGE_TITLE, 88 | $config 89 | ); 90 | } 91 | 92 | protected function editConfigPage( string $config ): void { 93 | $this->editPage( 94 | 'MediaWiki:' . WikibaseRdfExtension::CONFIG_PAGE_TITLE, 95 | $config 96 | ); 97 | } 98 | 99 | } 100 | --------------------------------------------------------------------------------