├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── ci.yml │ └── installMediaWiki.sh ├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── composer.json ├── extension.json ├── i18n ├── ar.json ├── bn.json ├── cy.json ├── da.json ├── de.json ├── en.json ├── es.json ├── fr.json ├── he.json ├── hu.json ├── ia.json ├── id.json ├── io.json ├── it.json ├── ko.json ├── lb.json ├── mk.json ├── my.json ├── nb.json ├── nl.json ├── pt-br.json ├── pt.json ├── qqq.json ├── roa-tara.json ├── ru.json ├── se.json ├── sl.json ├── sms.json ├── sv.json ├── tr.json ├── uk.json ├── uz.json ├── zh-hans.json └── zh-hant.json ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml.dist ├── resources ├── LocalMediaType.js ├── jquery.ui.mediasuggester.css └── jquery.ui.mediasuggester.js ├── src ├── HookHandlers.php ├── Services │ ├── FormatterBuilder.php │ ├── ImageLinkFormatter.php │ ├── ImageLinker.php │ ├── InlineImageFormatter.php │ ├── LocalImageLinker.php │ └── LocalMediaRdfBuilder.php └── WikibaseLocalMedia.php └── tests ├── js └── jquery.ui.mediasuggester.tests.js └── php ├── Integration ├── FormattingTest.php └── ValidationTest.php └── Unit ├── ImageLinkFormatterTest.php ├── LocalImageLinkerTest.php └── LocalMediaRdfBuilderTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/youtube.png export-ignore 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: JeroenDeDauw 4 | patreon: 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Setup 11 | 12 | - Wikibase Local Media version: 13 | - Wikibase version: 14 | - MediaWiki version: 15 | - PHP version: 16 | 17 | ### Issue 18 | 19 | Detailed description of the issue and a [stack trace](https://www.semantic-mediawiki.org/wiki/Help:Identifying_bugs) if applicable: 20 | 21 | ``` 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: [ "*" ] 8 | pull_request: 9 | branches: [ "*" ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | test: 14 | name: "PHPUnit: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 15 | continue-on-error: ${{ matrix.experimental }} 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - mw: 'REL1_40' 22 | php: 7.4 23 | experimental: false 24 | - mw: 'REL1_41' 25 | php: 8.1 26 | experimental: false 27 | - mw: 'REL1_42' 28 | php: 8.2 29 | experimental: false 30 | - mw: 'REL1_43' 31 | php: 8.3 32 | experimental: false 33 | - mw: 'master' 34 | php: 8.4 35 | experimental: true 36 | 37 | runs-on: ubuntu-latest 38 | 39 | defaults: 40 | run: 41 | working-directory: mediawiki 42 | 43 | # Steps represent a sequence of tasks that will be executed as part of the job 44 | steps: 45 | - name: Setup PHP 46 | uses: shivammathur/setup-php@v2 47 | with: 48 | php-version: ${{ matrix.php }} 49 | extensions: mbstring, intl 50 | tools: composer:v2 51 | 52 | - name: Cache MediaWiki 53 | id: cache-mediawiki 54 | uses: actions/cache@v4 55 | with: 56 | path: | 57 | mediawiki 58 | !mediawiki/extensions/ 59 | !mediawiki/vendor/ 60 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v1 61 | 62 | - name: Cache Composer cache 63 | uses: actions/cache@v4 64 | with: 65 | path: ~/.composer/cache 66 | key: composer-php${{ matrix.php }} 67 | 68 | - uses: actions/checkout@v4 69 | with: 70 | path: EarlyCopy 71 | 72 | - name: Install MediaWiki 73 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 74 | working-directory: ~ 75 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseLocalMedia 76 | 77 | - uses: actions/checkout@v4 78 | with: 79 | path: mediawiki/extensions/WikibaseLocalMedia 80 | 81 | - name: Composer update 82 | run: composer update 83 | 84 | - name: Run PHPUnit 85 | run: php tests/phpunit/phpunit.php extensions/WikibaseLocalMedia/tests/ 86 | 87 | PHPStan: 88 | name: "PHPStan: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 89 | 90 | strategy: 91 | matrix: 92 | include: 93 | - mw: 'REL1_43' 94 | php: '8.3' 95 | 96 | runs-on: ubuntu-latest 97 | 98 | defaults: 99 | run: 100 | working-directory: mediawiki 101 | 102 | steps: 103 | - name: Setup PHP 104 | uses: shivammathur/setup-php@v2 105 | with: 106 | php-version: ${{ matrix.php }} 107 | extensions: mbstring 108 | tools: composer, cs2pr 109 | 110 | - name: Cache MediaWiki 111 | id: cache-mediawiki 112 | uses: actions/cache@v4 113 | with: 114 | path: | 115 | mediawiki 116 | mediawiki/extensions/ 117 | mediawiki/vendor/ 118 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v1 119 | 120 | - name: Cache Composer cache 121 | uses: actions/cache@v4 122 | with: 123 | path: ~/.composer/cache 124 | key: composer_static_analysis 125 | 126 | - uses: actions/checkout@v4 127 | with: 128 | path: EarlyCopy 129 | 130 | - name: Install MediaWiki 131 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 132 | working-directory: ~ 133 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseLocalMedia 134 | 135 | - uses: actions/checkout@v4 136 | with: 137 | path: mediawiki/extensions/WikibaseLocalMedia 138 | 139 | - name: Composer allow-plugins 140 | run: composer config --no-plugins allow-plugins.composer/installers true 141 | 142 | - run: composer update 143 | 144 | - name: Composer install 145 | run: cd extensions/WikibaseLocalMedia && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader 146 | 147 | - name: PHPStan 148 | run: cd extensions/WikibaseLocalMedia && php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress | cs2pr 149 | 150 | phpcs: 151 | name: "Code style: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}" 152 | 153 | strategy: 154 | matrix: 155 | include: 156 | - mw: 'REL1_43' 157 | php: '8.3' 158 | 159 | runs-on: ubuntu-latest 160 | 161 | defaults: 162 | run: 163 | working-directory: mediawiki/extensions/WikibaseLocalMedia 164 | 165 | steps: 166 | - name: Setup PHP 167 | uses: shivammathur/setup-php@v2 168 | with: 169 | php-version: ${{ matrix.php }} 170 | extensions: mbstring, intl, php-ast 171 | tools: composer 172 | 173 | - name: Cache MediaWiki 174 | id: cache-mediawiki 175 | uses: actions/cache@v4 176 | with: 177 | path: | 178 | mediawiki 179 | !mediawiki/extensions/ 180 | !mediawiki/vendor/ 181 | key: mw_static_analysis 182 | 183 | - name: Cache Composer cache 184 | uses: actions/cache@v4 185 | with: 186 | path: ~/.composer/cache 187 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v1 188 | 189 | - uses: actions/checkout@v4 190 | with: 191 | path: EarlyCopy 192 | 193 | - name: Install MediaWiki 194 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 195 | working-directory: ~ 196 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseLocalMedia 197 | 198 | - uses: actions/checkout@v4 199 | with: 200 | path: mediawiki/extensions/WikibaseLocalMedia 201 | 202 | - name: Composer install 203 | run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader 204 | 205 | - run: vendor/bin/phpcs -p -s 206 | -------------------------------------------------------------------------------- /.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 | echo '$wgServer = "http://localhost";' >> LocalSettings.php 22 | echo '$wgDeprecationReleaseLimit = "1.33";' >> LocalSettings.php 23 | 24 | echo '$wgEnableWikibaseRepo = true;' >> LocalSettings.php 25 | echo '$wgEnableWikibaseClient = false;' >> LocalSettings.php 26 | 27 | if [ "$MW_BRANCH" == "REL1_34" ] || [ "$MW_BRANCH" == "REL1_35" ]; then 28 | echo 'require_once __DIR__ . "/extensions/Wikibase/repo/Wikibase.php";' >> LocalSettings.php 29 | else 30 | echo 'wfLoadExtension( "WikibaseRepository", __DIR__ . "/extensions/Wikibase/extension-repo.json" );' >> LocalSettings.php 31 | fi 32 | 33 | echo 'require_once __DIR__ . "/extensions/Wikibase/repo/ExampleSettings.php";' >> LocalSettings.php 34 | 35 | echo 'wfLoadExtension( "'$EXTENSION_NAME'" );' >> LocalSettings.php 36 | 37 | cat <> composer.local.json 38 | { 39 | "require": { 40 | "wikibase/wikibase": "dev-$MW_BRANCH" 41 | }, 42 | "extra": { 43 | "merge-plugin": { 44 | "merge-dev": true, 45 | "include": [ 46 | "extensions/$EXTENSION_NAME/composer.json" 47 | ] 48 | } 49 | } 50 | } 51 | EOT 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Commands run from inside the MediaWiki container ########################################################## 2 | 3 | ci: test cs 4 | test: phpunit 5 | cs: phpcs stan 6 | 7 | phpunit: 8 | ifdef filter 9 | php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist --filter $(filter) 10 | else 11 | php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist 12 | endif 13 | 14 | perf: 15 | php ../../tests/phpunit/phpunit.php -c phpunit.xml.dist --group Performance 16 | 17 | phpcs: 18 | vendor/bin/phpcs -p -s --standard=$(shell pwd)/phpcs.xml 19 | 20 | stan: 21 | vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G 22 | 23 | stan-baseline: 24 | vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=2G --generate-baseline 25 | 26 | 27 | 28 | # Commands run from the host machine ####################################################################### 29 | 30 | npm-install: 31 | docker run -it --rm -v "$(CURDIR)":/home/node/app -w /home/node/app -u node node:22 npm install 32 | 33 | lint: 34 | docker run -it --rm -v "$(CURDIR)":/home/node/app -w /home/node/app -u node node:22 npm run lint 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikibase Local Media 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ProfessionalWiki/WikibaseLocalMedia/ci.yml?branch=master)](https://github.com/ProfessionalWiki/WikibaseLocalMedia/actions?query=workflow%3ACI) 4 | [![Latest Stable Version](https://poser.pugx.org/professional-wiki/wikibase-local-media/v/stable)](https://packagist.org/packages/professional-wiki/wikibase-local-media) 5 | [![Download count](https://poser.pugx.org/professional-wiki/wikibase-local-media/downloads)](https://packagist.org/packages/professional-wiki/wikibase-local-media) 6 | [![License](https://poser.pugx.org/professional-wiki/wikibase-local-media/license)](LICENSE) 7 | 8 | MediaWiki extension that adds support for local media files to [Wikibase] via a new data type. 9 | 10 | [Professional Wiki] created and maintains Wikibase Local Media. We provide [Wikibase hosting], [Wikibase development], and [Wikibase consulting]. 11 | 12 | [Rhizome] commissioned and funded the extension's initial development. Wikibase Local Media is an open-source project, and contributions are welcome! 13 | 14 | ## Demo and screenshot 15 | 16 | 17 | 18 | 19 | 20 | 21 | Also see [this animated gif](https://twitter.com/i/status/1286293710112731137). 22 | 23 | ## Platform requirements 24 | 25 | * [PHP] 7.4 or later 26 | * [MediaWiki] 1.40 or later 27 | * [Wikibase Repository] 28 | 29 | For more information on the different versions of this extension, see the [release notes](#release-notes). 30 | 31 | ## Installation 32 | 33 | First, install MediaWiki and Wikibase Repository. 34 | 35 | **Using Composer (option 1/2)** 36 | 37 | The recommended way to install Wikibase Local Media is using [Composer](https://getcomposer.org) with 38 | [MediaWiki's built-in support for Composer](https://professional.wiki/en/articles/installing-mediawiki-extensions-with-composer). 39 | 40 | On the commandline, go to your wiki's root directory. Then run these two commands: 41 | 42 | ```shell script 43 | COMPOSER=composer.local.json composer require --no-update professional-wiki/wikibase-local-media:* 44 | composer update professional-wiki/wikibase-local-media --no-dev -o 45 | ``` 46 | 47 | **Manual download (option 2/2)** 48 | 49 | You can also install the extension via git clone or download. Place the `WikibaseLocalMedia` directory into `extensions`. 50 | 51 | **Enabling the extension** 52 | 53 | Then enable the extension by adding the following to the bottom of your wikis `LocalSettings.php` file: 54 | 55 | ```php 56 | wfLoadExtension( 'WikibaseLocalMedia' ); 57 | ``` 58 | 59 | You can verify the extension was enabled successfully by opening your wikis Special:Version page in your browser. 60 | 61 | ## PHP Configuration 62 | 63 | Configuration can be changed via [LocalSettings.php]. 64 | 65 | ### Setting foreign file repo 66 | 67 | Optional wiki API URL, works in conjunction with `$wgForeignFileRepos` for retrieving images from any wiki 68 | 69 | Variable: `$wgWikibaseLocalMediaRemoteApiUrl` 70 | 71 | Default: `null` 72 | 73 | Example: `https://commons.wikimedia.org/w/api.php` 74 | 75 | ## Running the tests 76 | 77 | * PHP tests: `php tests/phpunit/phpunit.php extensions/WikibaseLocalMedia/tests/` 78 | * JS tests: `index.php?title=Special%3AJavaScriptTest&filter=jquery.ui.mediasuggester` 79 | 80 | ## Release notes 81 | 82 | ### Version 2.0.0 83 | 84 | Released on March 30, 2025 85 | 86 | * Raised the minimum MediaWiki version from 1.35 to 1.40 87 | * Added support for MediaWiki 1.41, 1.42, 1.43, and the development version of 1.44 88 | * Translation updates 89 | 90 | ### Version 1.1.0 91 | 92 | Released on February 16, 2025 93 | 94 | * [Added optional APU URL configuration variable](https://github.com/ProfessionalWiki/WikibaseLocalMedia/pull/37) `wgWikibaseLocalMediaRemoteApiUrl` (works together with `wgForeignFileRepos`) 95 | * Translation updates 96 | 97 | ### Version 1.0.4 98 | 99 | Released on October 9, 2024 100 | 101 | * Fixed support for non-English wikis 102 | * Fixed deprecation warning of MediaWiki 1.40 and later 103 | * Translation updates 104 | 105 | ### Version 1.0.3 106 | 107 | Released on March 30, 2023 108 | 109 | * Added support for MediaWiki and Wikibase 1.38 and 1.39 110 | * Translation updates 111 | 112 | ### Version 1.0.2 113 | 114 | Released on October 4th 2022 115 | 116 | * Added support for MediaWiki and Wikibase 1.37 117 | * Translation updates 118 | 119 | ### Version 1.0.1 120 | 121 | Released on March 17th 2021 122 | 123 | * Allowed installation with PHP 7.2.x 124 | * Translation updates 125 | 126 | ### Version 1.0.0 127 | 128 | Released on October 5th 2020 129 | 130 | * Added optional integration with Wikibase Client 131 | * Translation updates 132 | 133 | ### Version 0.2.1 134 | 135 | Released on September 28th 2020 136 | 137 | * Fixed support for PHP 7.3.x 138 | 139 | ### Version 0.2 140 | 141 | Released on September 26th 2020 142 | 143 | * Added support for MediaWiki/Wikibase 1.35 144 | * The extension is now listed in the Wikibase group on Special:Version 145 | 146 | ### Version 0.1 147 | 148 | Released on September 26th 2020 149 | 150 | * [Initial release] for MediaWiki/Wikibase 1.34 151 | 152 | [Professional Wiki]: https://professional.wiki 153 | [Wikibase]: https://professional.wiki/en/wikibase-wikidata-and-knowledge-graphs 154 | [Wikibase hosting]: https://professional.wiki/en/hosting/wikibase 155 | [Wikibase development]: https://professional.wiki/en/wikibase-software-development 156 | [Wikibase consulting]: https://wikibase.consulting/ 157 | [Rhizome]: https://rhizome.org/ 158 | [MediaWiki]: https://www.mediawiki.org 159 | [PHP]: https://www.php.net 160 | [Wikibase Repository]: https://www.mediawiki.org/wiki/Extension:Wikibase_Repository 161 | [LocalSettings.php]: https://www.mediawiki.org/wiki/Manual:LocalSettings.php 162 | [Initial release]: https://professional.wiki/en/news/wikibase-local-media 163 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "professional-wiki/wikibase-local-media", 3 | "type": "mediawiki-extension", 4 | "description": "Adds the local media data type to Wikibase", 5 | "keywords": [ 6 | "MediaWiki", 7 | "Wikibase", 8 | "local media", 9 | "localmedia", 10 | "images", 11 | "files", 12 | "datatype", 13 | "extension", 14 | "wiki", 15 | "structured data", 16 | "semantic web", 17 | "wikidata" 18 | ], 19 | "homepage": "https://professional.wiki/en/extension/wikibase-local-media", 20 | "license": "GPL-2.0-or-later", 21 | "authors": [ 22 | { 23 | "name": "Professional Wiki", 24 | "email": "info@professional.wiki", 25 | "homepage": "https://Professional.Wiki", 26 | "role": "Creator" 27 | }, 28 | { 29 | "name": "Jeroen De Dauw", 30 | "email": "jeroendedauw@gmail.com", 31 | "homepage": "https://www.EntropyWins.wtf/mediawiki", 32 | "role": "Creator and developer" 33 | }, 34 | { 35 | "name": "Jonas Kress", 36 | "role": "Creator and developer" 37 | }, 38 | { 39 | "name": "Wikidata Team", 40 | "role": "Author of copied code" 41 | } 42 | ], 43 | "support": { 44 | "issues": "https://github.com/ProfessionalWiki/WikibaseLocalMedia/issues", 45 | "source": "https://github.com/ProfessionalWiki/WikibaseLocalMedia" 46 | }, 47 | "require": { 48 | "php": ">=7.4", 49 | "composer/installers": "^2|^1.0.1" 50 | }, 51 | "require-dev": { 52 | "phpstan/phpstan": "^2.1.6", 53 | "mediawiki/mediawiki-codesniffer": "^46.0.0" 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "composer/installers": true, 58 | "dealerdirect/phpcodesniffer-composer-installer": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wikibase Local Media", 3 | "type": "wikibase", 4 | 5 | "version": "2.0.0", 6 | 7 | "author": [ 8 | "[https://www.EntropyWins.wtf/mediawiki Jeroen De Dauw]", 9 | "[https://professional.wiki/ Professional Wiki]" 10 | ], 11 | 12 | "url": "https://professional.wiki/en/extension/wikibase-local-media", 13 | 14 | "descriptionmsg": "wblm-desc", 15 | 16 | "license-name": "GPL-2.0-or-later", 17 | 18 | "requires": { 19 | "MediaWiki": ">= 1.40.0", 20 | "extensions": { 21 | "WikibaseRepository": "*" 22 | } 23 | }, 24 | 25 | "MessagesDirs": { 26 | "WikibaseLocalMedia": [ 27 | "i18n" 28 | ] 29 | }, 30 | 31 | "AutoloadNamespaces": { 32 | "Wikibase\\LocalMedia\\": "src/", 33 | "Wikibase\\LocalMedia\\Tests\\": "tests/" 34 | }, 35 | 36 | "Hooks": { 37 | "WikibaseRepoDataTypes": "Wikibase\\LocalMedia\\HookHandlers::onWikibaseRepoDataTypes", 38 | "WikibaseClientDataTypes": "Wikibase\\LocalMedia\\HookHandlers::onWikibaseClientDataTypes", 39 | "ResourceLoaderGetConfigVars": "Wikibase\\LocalMedia\\HookHandlers::onResourceLoaderGetConfigVars" 40 | }, 41 | 42 | "config": { 43 | "WikibaseLocalMediaRemoteApiUrl": { 44 | "value": null, 45 | "description": "Optional wiki API URL, works in conjunction with $wgForeignFileRepos for retrieving images from any wiki" 46 | } 47 | }, 48 | 49 | "ResourceFileModulePaths": { 50 | "localBasePath": "resources", 51 | "remoteExtPath": "WikibaseLocalMedia/resources" 52 | }, 53 | 54 | "ResourceModules": { 55 | "jquery.ui.mediasuggester": { 56 | "scripts": [ 57 | "jquery.ui.mediasuggester.js" 58 | ], 59 | "styles": [ 60 | "jquery.ui.mediasuggester.css" 61 | ], 62 | "dependencies": [ 63 | "jquery.ui.suggester", 64 | "jquery.ui", 65 | "util.highlightSubstring" 66 | ], 67 | "targets": [ "desktop", "mobile" ] 68 | }, 69 | "jquery.valueview.experts.LocalMediaType": { 70 | "scripts": [ 71 | "LocalMediaType.js" 72 | ], 73 | "dependencies": [ 74 | "jquery.event.special.eachchange", 75 | "jquery.ui.mediasuggester", 76 | "jquery.valueview.experts.StringValue", 77 | "jquery.valueview.Expert" 78 | ], 79 | "targets": [ "desktop", "mobile" ] 80 | } 81 | }, 82 | 83 | "QUnitTestModule": { 84 | "localBasePath": "tests/js", 85 | "remoteExtPath": "WikibaseLocalMedia/tests/js", 86 | "scripts": [ 87 | "jquery.ui.mediasuggester.tests.js" 88 | ], 89 | "dependencies": [ 90 | "jquery.ui.mediasuggester" 91 | ] 92 | }, 93 | 94 | "manifest_version": 2 95 | } 96 | -------------------------------------------------------------------------------- /i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Meno25" 5 | ] 6 | }, 7 | "wblm-desc": "يضيف دعمًا لملفات الوسائط المحلية إلى Wikibase عبر نوع بيانات مخصص", 8 | "datatypes-type-localMedia": "ملف وسائط" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "আফতাবুজ্জামান" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "মিডিয়া ফাইল" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/cy.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Robin Owain" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Ffeil cyfrwng" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Saederup92" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Medie filer" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kghbln", 5 | "Killarnee" 6 | ] 7 | }, 8 | "wblm-desc": "Ermöglicht Wikibase die Nutzung lokaler Mediendateien mithilfe eines eigenen Datentyps", 9 | "datatypes-type-localMedia": "Mediendatei" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Jeroen De Dauw" 5 | ] 6 | }, 7 | "wblm-name": "Wikibase Local Media", 8 | "wblm-desc": "Adds support for local media files to Wikibase via a dedicated data type", 9 | 10 | "datatypes-type-localMedia": "Media file" 11 | } 12 | -------------------------------------------------------------------------------- /i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Fitoschido" 5 | ] 6 | }, 7 | "wblm-desc": "Permite añadir archivos multimedia locales a Wikibase a través de un tipo de datos especializado", 8 | "datatypes-type-localMedia": "Archivo multimedia" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Gomoko", 5 | "Verdy p" 6 | ] 7 | }, 8 | "wblm-desc": "Ajoute la prise en charge des fichiers de médias locaux vers Wikibase via un type de données dédié", 9 | "datatypes-type-localMedia": "Fichier de média" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Amire80", 5 | "YaronSh" 6 | ] 7 | }, 8 | "wblm-desc": "הוספת תמיכה בקובצי מדיה מקומיים לוויקיבייס באמצעות סוג נתונים ייעודי", 9 | "datatypes-type-localMedia": "קובץ מדיה" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hanna Tardos" 5 | ] 6 | }, 7 | "wblm-desc": "Támogatás Wikibase-en belül a helyi médiafájlok számára megfelelő adattípussal", 8 | "datatypes-type-localMedia": "Médiafájl" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/ia.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie" 5 | ] 6 | }, 7 | "wblm-desc": "Adde supporto pro files multimedial local a Wikibase per medio de un typo de datos dedicate.", 8 | "datatypes-type-localMedia": "File multimedial" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "RXerself" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Berkas media" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/io.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Joao Xavier" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Tipo di arkivo" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Beta16" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "File multimediale" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kwj2772", 5 | "Ykhwong" 6 | ] 7 | }, 8 | "wblm-desc": "전용 데이터 유형을 통해 위키베이스에 로컬 미디어 파일에 대한 지원 기능을 추가합니다", 9 | "datatypes-type-localMedia": "미디어 파일" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Robby" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Media-Fichier" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/mk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Bjankuloski06" 5 | ] 6 | }, 7 | "wblm-desc": "Додава поддршка за месни медиумски податотеки во Викибазата преку наменски податочен тип", 8 | "datatypes-type-localMedia": "Медиумска податотека" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/my.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Dr Lotus Black" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "မီဒီယာဖိုင်" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Jon Harald Søby" 5 | ] 6 | }, 7 | "wblm-desc": "Legger til støtte for lokale mediefiler i Wikibase via en dedikert datatype", 8 | "datatypes-type-localMedia": "Mediefil" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie", 5 | "Romaine" 6 | ] 7 | }, 8 | "wblm-desc": "Voegt ondersteuning voor lokale mediabestanden toe aan Wikibase via een speciaal gegevenstype", 9 | "datatypes-type-localMedia": "Mediabestand" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eduardo Addad de Oliveira", 5 | "Eduardoaddad" 6 | ] 7 | }, 8 | "wblm-desc": "Adiciona suporte para arquivos de mídia local à Wikibase por meio de um tipo de dados dedicado", 9 | "datatypes-type-localMedia": "Arquivo de mídia" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hamilton Abreu" 5 | ] 6 | }, 7 | "wblm-desc": "Adiciona a Wikibase suporte de ficheiros multimédia locais por meio de um tipo de dados especializado", 8 | "datatypes-type-localMedia": "Ficheiro multimédia" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/qqq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Jeroen De Dauw", 5 | "Kghbln" 6 | ] 7 | }, 8 | "wblm-name": "{{Name}}", 9 | "wblm-desc": "{{Desc|name=Wikibase Local Media|url=https://github.com/ProfessionalWiki/WikibaseLocalMedia}}", 10 | "datatypes-type-localMedia": "This is the name of the datatype." 11 | } 12 | -------------------------------------------------------------------------------- /i18n/roa-tara.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Joetaras" 5 | ] 6 | }, 7 | "wblm-desc": "Aggiunge 'u supporte pe le file media locale a Uicchibase cu 'nu tipe de date dedicate", 8 | "datatypes-type-localMedia": "File multimediale" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yurina Tatiana" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Медиа-файл" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/se.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yupik" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Mediafiila" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eleassar" 5 | ] 6 | }, 7 | "wblm-desc": "V Wikibase z namensko vrsto podatkov doda podporo za lokalne predstavnostne datoteke", 8 | "datatypes-type-localMedia": "Predstavnostna datoteka" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/sms.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Yupik" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Mediateâttõs" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Sabelöga", 5 | "WikiPhoenix" 6 | ] 7 | }, 8 | "wblm-desc": "Lägger till stöd för lokala mediefiler i Wikibase via en dedikerad datatyp", 9 | "datatypes-type-localMedia": "Mediafil" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "MuratTheTurkish" 5 | ] 6 | }, 7 | "wblm-desc": "Özel bir veri türü üzerinden Wikibase'e yerel medya dosyaları için destek ekler", 8 | "datatypes-type-localMedia": "Medya dosyası" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "DDPAT", 5 | "Ice bulldog" 6 | ] 7 | }, 8 | "wblm-desc": "Додає підтримку локальних мультимедійних файлів у Вікіданих через виділений тип даних", 9 | "datatypes-type-localMedia": "Медіафайл" 10 | } 11 | -------------------------------------------------------------------------------- /i18n/uz.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Zafar Shamsiddinov" 5 | ] 6 | }, 7 | "datatypes-type-localMedia": "Media fayl" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Shizhao" 5 | ] 6 | }, 7 | "wblm-desc": "通过专用数据类型向Wikibase添加对本地媒体文件的支持", 8 | "datatypes-type-localMedia": "媒体文件" 9 | } 10 | -------------------------------------------------------------------------------- /i18n/zh-hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kly" 5 | ] 6 | }, 7 | "wblm-desc": "通過專用資料類型,添加本地端多媒體檔案的支援到 Wikibase", 8 | "datatypes-type-localMedia": "多媒體檔案" 9 | } 10 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | src/ 4 | tests/php 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Access to undefined constant Wikibase\\Repo\\Rdf\\PropertyRdfBuilder\:\:OBJECT_PROPERTY\.$#' 5 | identifier: classConstant.notFound 6 | count: 1 7 | path: src/HookHandlers.php 8 | 9 | - 10 | message: '#^Method Wikibase\\LocalMedia\\HookHandlers\:\:onResourceLoaderGetConfigVars\(\) has parameter \$vars with no value type specified in iterable type array\.$#' 11 | identifier: missingType.iterableValue 12 | count: 1 13 | path: src/HookHandlers.php 14 | 15 | - 16 | message: '#^Method Wikibase\\LocalMedia\\HookHandlers\:\:onWikibaseClientDataTypes\(\) has parameter \$dataTypeDefinitions with no value type specified in iterable type array\.$#' 17 | identifier: missingType.iterableValue 18 | count: 1 19 | path: src/HookHandlers.php 20 | 21 | - 22 | message: '#^Method Wikibase\\LocalMedia\\HookHandlers\:\:onWikibaseRepoDataTypes\(\) has parameter \$dataTypeDefinitions with no value type specified in iterable type array\.$#' 23 | identifier: missingType.iterableValue 24 | count: 1 25 | path: src/HookHandlers.php 26 | 27 | - 28 | message: '#^Method Wikibase\\LocalMedia\\Services\\FormatterBuilder\:\:__construct\(\) has parameter \$thumbLimits with no value type specified in iterable type array\.$#' 29 | identifier: missingType.iterableValue 30 | count: 1 31 | path: src/Services/FormatterBuilder.php 32 | 33 | - 34 | message: '#^Parameter \#3 \$languageCode of class Wikibase\\LocalMedia\\Services\\InlineImageFormatter constructor expects string, mixed given\.$#' 35 | identifier: argument.type 36 | count: 1 37 | path: src/Services/FormatterBuilder.php 38 | 39 | - 40 | message: '#^Property Wikibase\\LocalMedia\\Services\\FormatterBuilder\:\:\$thumbLimits type has no value type specified in iterable type array\.$#' 41 | identifier: missingType.iterableValue 42 | count: 1 43 | path: src/Services/FormatterBuilder.php 44 | 45 | - 46 | message: '#^Instanceof between DataValues\\StringValue and DataValues\\StringValue will always evaluate to true\.$#' 47 | identifier: instanceof.alwaysTrue 48 | count: 1 49 | path: src/Services/ImageLinkFormatter.php 50 | 51 | - 52 | message: '#^Constructor of class Wikibase\\LocalMedia\\Services\\InlineImageFormatter has an unused parameter \$languageCode\.$#' 53 | identifier: constructor.unusedParameter 54 | count: 1 55 | path: src/Services/InlineImageFormatter.php 56 | 57 | - 58 | message: '#^Instanceof between DataValues\\StringValue and DataValues\\StringValue will always evaluate to true\.$#' 59 | identifier: instanceof.alwaysTrue 60 | count: 1 61 | path: src/Services/InlineImageFormatter.php 62 | 63 | - 64 | message: '#^Method Wikibase\\LocalMedia\\Services\\InlineImageFormatter\:\:__construct\(\) has parameter \$thumbLimits with no value type specified in iterable type array\.$#' 65 | identifier: missingType.iterableValue 66 | count: 1 67 | path: src/Services/InlineImageFormatter.php 68 | 69 | - 70 | message: '#^Property Wikibase\\LocalMedia\\Services\\InlineImageFormatter\:\:\$thumbLimits type has no value type specified in iterable type array\.$#' 71 | identifier: missingType.iterableValue 72 | count: 1 73 | path: src/Services/InlineImageFormatter.php 74 | 75 | - 76 | message: '#^Cannot call method getFullURL\(\) on MediaWiki\\Title\\Title\|null\.$#' 77 | identifier: method.nonObject 78 | count: 1 79 | path: src/Services/LocalMediaRdfBuilder.php 80 | 81 | - 82 | message: '#^Parameter \#1 \$text of method MediaWiki\\Title\\TitleFactory\:\:newFromText\(\) expects int\|string\|null, mixed given\.$#' 83 | identifier: argument.type 84 | count: 1 85 | path: src/Services/LocalMediaRdfBuilder.php 86 | 87 | - 88 | message: '#^Parameter \#2 \$thumbLimits of class Wikibase\\LocalMedia\\Services\\FormatterBuilder constructor expects array, mixed given\.$#' 89 | identifier: argument.type 90 | count: 1 91 | path: src/WikibaseLocalMedia.php -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | 9 | src 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/LocalMediaType.js: -------------------------------------------------------------------------------- 1 | module.exports = ( function( $, vv ) { 2 | 'use strict'; 3 | 4 | var PARENT = vv.experts.StringValue; 5 | 6 | vv.experts.LocalMediaType = vv.expert( 'LocalMediaType', PARENT, { 7 | /** 8 | * @inheritdoc 9 | * @protected 10 | */ 11 | _init: function() { 12 | PARENT.prototype._init.call( this ); 13 | 14 | var notifier = this._viewNotifier, 15 | $input = this.$input; 16 | 17 | const apiUrl = mw.config.get( 'wgWikibaseLocalMediaRemoteApiUrl' ) || this._options.vocabularyLookupApiUrl; 18 | $input.mediasuggester( { 19 | apiUrl: apiUrl, 20 | indexPhpUrl: apiUrl.replace('api.php', 'index.php'), 21 | namespaceId: 6 22 | } ); 23 | 24 | // Using the inputautoexpand plugin, the position of the dropdown needs to be updated 25 | // whenever the input box expands vertically: 26 | $input 27 | .on( 'eachchange', function( event, oldValue ) { 28 | $input.data( 'mediasuggester' ).repositionMenu(); 29 | } ) 30 | .on( 'mediasuggesterchange', function( event, response ) { 31 | notifier.notify( 'change' ); 32 | $input.data( 'inputautoexpand' ).expand(); 33 | } ); 34 | } 35 | } ); 36 | 37 | return vv.experts.LocalMediaType; 38 | 39 | }( jQuery, jQuery.valueview ) ); 40 | -------------------------------------------------------------------------------- /resources/jquery.ui.mediasuggester.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license GNU GPL v2+ 3 | */ 4 | .ui-mediasuggester-list a { 5 | max-width: 50em; 6 | overflow: hidden; 7 | padding-left: 0; 8 | -o-text-overflow: ellipsis; /* Opera 9 to 10 */ 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | } 12 | 13 | .ui-mediasuggester-list a .ui-mediasuggester-thumbnail { 14 | background-color: #eee; 15 | background-position: center; 16 | background-size: auto 50px; 17 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.3) inset; 18 | color: #999; 19 | display: inline-block; 20 | height: 50px; 21 | line-height: 50px; 22 | margin-right: 0.5em; 23 | overflow: hidden; 24 | text-align: center; 25 | vertical-align: middle; 26 | white-space: nowrap; 27 | width: 50px; 28 | } 29 | -------------------------------------------------------------------------------- /resources/jquery.ui.mediasuggester.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 'use strict'; 3 | 4 | /** 5 | * Media suggester. 6 | * Enhances an input box with suggestion functionality for local asset names. 7 | * (uses `util.highlightSubstring`) 8 | * @class jQuery.ui.mediasuggester 9 | * @extends jQuery.ui.suggester 10 | * @uses util 11 | * @license GNU GPL v2+ 12 | * 13 | * @constructor 14 | */ 15 | $.widget( 'ui.mediasuggester', $.ui.suggester, { 16 | 17 | /** 18 | * @see jQuery.ui.suggester.options 19 | */ 20 | options: { 21 | ajax: $.ajax, 22 | apiUrl: null, 23 | indexPhpUrl: null, 24 | namespaceId: 6 25 | }, 26 | 27 | /** 28 | * @inheritdoc 29 | * @protected 30 | */ 31 | _create: function() { 32 | if ( !this.options.apiUrl ) { 33 | throw new Error( 'apiUrl option required' ); 34 | } 35 | 36 | if ( !this.options.source ) { 37 | this.options.source = this._initDefaultSource(); 38 | } 39 | 40 | $.ui.suggester.prototype._create.call( this ); 41 | 42 | this.options.menu.element.addClass( 'ui-mediasuggester-list' ); 43 | }, 44 | 45 | /** 46 | * Initializes the default source pointing to the "query" API module of the local wiki. 47 | * @protected 48 | * 49 | * @return {Function} 50 | */ 51 | _initDefaultSource: function() { 52 | var self = this; 53 | 54 | return function( term ) { 55 | var deferred = $.Deferred(); 56 | 57 | self.options.ajax( { 58 | url: self.options.apiUrl, 59 | dataType: 'jsonp', 60 | data: { 61 | action: 'query', 62 | list: 'search', 63 | srsearch: term, 64 | srnamespace: self.options.namespaceId, 65 | srlimit: 10, 66 | format: 'json' 67 | }, 68 | timeout: 8000 69 | } ) 70 | .done( function( response ) { 71 | if ( response.query === undefined ) { 72 | return; 73 | } 74 | var sorted = self._prioritiseMatchingFilename( response.query.search, term ); 75 | 76 | deferred.resolve( sorted, term ); 77 | } ) 78 | .fail( function( jqXHR, textStatus ) { 79 | // Since this is a JSONP request, this will always fail with a timeout... 80 | deferred.reject( textStatus ); 81 | } ); 82 | 83 | return deferred.promise(); 84 | }; 85 | }, 86 | 87 | /** 88 | * Be smart on the media search results and put an exactly matching file name on top 89 | * @private 90 | * 91 | * @param {Array} resultList Results from the search API response 92 | * @param {string} term The user's search term 93 | * 94 | * @return {Array} 95 | */ 96 | _prioritiseMatchingFilename: function( resultList, term ) { 97 | return resultList.sort( function( a, b ) { 98 | // use indexOf() in favour of startsWith() for browser compatibility 99 | if ( a.title.indexOf( this._getFileNamespace() + ':' + term ) === 0 ) { 100 | return -1; 101 | } else if ( b.title.indexOf( this._getFileNamespace() + ':' + term ) === 0 ) { 102 | return 1; 103 | } else { 104 | return 0; 105 | } 106 | } ); 107 | }, 108 | 109 | /** 110 | * @see jQuery.ui.suggester._createMenuItemFromSuggestion 111 | * @protected 112 | * 113 | * @param {Object} suggestion 114 | * @param {string} requestTerm 115 | * @return {jQuery.ui.ooMenu.Item} 116 | */ 117 | _createMenuItemFromSuggestion: function( suggestion, requestTerm ) { 118 | suggestion = suggestion.title; 119 | 120 | var isFile = this._getFileNamespaceRegex().test( suggestion ); 121 | 122 | if ( isFile ) { 123 | suggestion = suggestion.replace( this._getFileNamespaceRegex(), '' ); 124 | } 125 | 126 | var label = util.highlightSubstring( 127 | requestTerm, 128 | suggestion, 129 | { 130 | caseSensitive: false, 131 | withinString: true 132 | } 133 | ), 134 | $label = $( '' ) 135 | .attr( { dir: 'ltr', title: suggestion } ) 136 | .append( label ); 137 | 138 | if ( isFile ) { 139 | $label.prepend( this._createThumbnail( suggestion ) ); 140 | } 141 | 142 | return new $.ui.ooMenu.Item( $label, suggestion ); 143 | }, 144 | 145 | /** 146 | * @private 147 | * 148 | * @param {string} fileName Must be a file name without the File: namespace. 149 | * @return {jQuery} 150 | */ 151 | _createThumbnail: function( fileName ) { 152 | return $( '' ) 153 | .attr( 'class', 'ui-mediasuggester-thumbnail' ) 154 | .css( 'background-image', this._createBackgroundImage( fileName ) ); 155 | }, 156 | 157 | /** 158 | * @private 159 | * 160 | * @param {string} fileName Must be a file name without the File: namespace. 161 | * @return {string} CSS 162 | */ 163 | _createBackgroundImage: function ( fileName ) { 164 | // Height alone is ignored, width must be set to something. 165 | // We accept to truncate 50% and only show the center 50% of the images area. 166 | return 'url("' + this.options.indexPhpUrl + '?title=Special:Filepath/' 167 | + encodeURIComponent( fileName ) 168 | + '&width=100&height=50")'; 169 | }, 170 | 171 | /** 172 | * @private 173 | * 174 | * @returns {string} 175 | */ 176 | _getFileNamespace: function () { 177 | return mw.config.get('wgFormattedNamespaces')[6]; 178 | }, 179 | 180 | /** 181 | * @private 182 | * 183 | * @returns {RegExp} 184 | */ 185 | _getFileNamespaceRegex: function () { 186 | return new RegExp( '^' + this._getFileNamespace() + ':' ); 187 | } 188 | 189 | } ); 190 | 191 | }() ); 192 | -------------------------------------------------------------------------------- /src/HookHandlers.php: -------------------------------------------------------------------------------- 1 | 'string', 15 | 'expert-module' => 'jquery.valueview.experts.LocalMediaType', 16 | 'validator-factory-callback' => static function () { 17 | return WikibaseLocalMedia::getGlobalInstance()->getValueValidators(); 18 | }, 19 | 'formatter-factory-callback' => static function ( $format, FormatterOptions $options ) { 20 | return WikibaseLocalMedia::getGlobalInstance() 21 | ->getFormatterBuilder()->newFormatter( $format, $options ); 22 | }, 23 | 'rdf-builder-factory-callback' => static function () { 24 | return WikibaseLocalMedia::getGlobalInstance()->getRdfBuilder(); 25 | }, 26 | 'rdf-data-type' => static function () { 27 | /** @since MediaWiki 1.37 */ 28 | if ( class_exists( 'Wikibase\Repo\Rdf\PropertySpecificComponentsRdfBuilder' ) ) { 29 | return \Wikibase\Repo\Rdf\PropertySpecificComponentsRdfBuilder::OBJECT_PROPERTY; 30 | } 31 | 32 | return \Wikibase\Repo\Rdf\PropertyRdfBuilder::OBJECT_PROPERTY; 33 | }, 34 | ]; 35 | } 36 | 37 | public static function onWikibaseClientDataTypes( array &$dataTypeDefinitions ): void { 38 | $dataTypeDefinitions['PT:localMedia'] = [ 39 | 'value-type' => 'string', 40 | 'formatter-factory-callback' => static function ( $format, FormatterOptions $options ) { 41 | return WikibaseLocalMedia::getGlobalInstance() 42 | ->getFormatterBuilder()->newFormatter( $format, $options ); 43 | }, 44 | ]; 45 | } 46 | 47 | public static function onResourceLoaderGetConfigVars( array &$vars ): void { 48 | $config = MediaWikiServices::getInstance()->getMainConfig(); 49 | $vars['wgWikibaseLocalMediaRemoteApiUrl'] = $config->get( 'WikibaseLocalMediaRemoteApiUrl' ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Services/FormatterBuilder.php: -------------------------------------------------------------------------------- 1 | formatterBuilders = $formatterBuilders; 22 | $this->thumbLimits = $thumbLimits; 23 | } 24 | 25 | public function newFormatter( string $format, FormatterOptions $options ): ValueFormatter { 26 | $snakFormat = new SnakFormat(); 27 | 28 | if ( $snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_VERBOSE, $format ) ) { 29 | return new InlineImageFormatter( 30 | RequestContext::getMain()->getOutput()->parserOptions(), 31 | $this->thumbLimits, 32 | $options->getOption( ValueFormatter::OPT_LANG ), 33 | new LocalImageLinker(), 34 | 'commons-media-caption' 35 | ); 36 | } 37 | 38 | switch ( $snakFormat->getBaseFormat( $format ) ) { 39 | case SnakFormatter::FORMAT_HTML: 40 | return new ImageLinkFormatter( new LocalImageLinker(), '' ); 41 | case SnakFormatter::FORMAT_WIKI: 42 | return new CommonsThumbnailFormatter(); 43 | default: 44 | return $this->formatterBuilders->newStringFormatter( $format ); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/ImageLinkFormatter.php: -------------------------------------------------------------------------------- 1 | imageLinker = $imageLinker; 20 | $this->cssClass = $cssClass; 21 | } 22 | 23 | /** 24 | * @see ValueFormatter::format 25 | * 26 | * Formats the given commons file name as an HTML link 27 | * 28 | * @param StringValue $value The commons file name to turn into a link 29 | * 30 | * @throws InvalidArgumentException 31 | * @return string HTML 32 | */ 33 | public function format( $value ) { 34 | if ( !( $value instanceof StringValue ) ) { 35 | throw new InvalidArgumentException( 'Data value type mismatch. Expected a StringValue.' ); 36 | } 37 | 38 | $fileName = $value->getValue(); 39 | $title = Title::makeTitleSafe( NS_MAIN, $fileName ); 40 | 41 | if ( $title === null ) { 42 | return htmlspecialchars( $fileName ); 43 | } 44 | 45 | return Html::element( 46 | 'a', 47 | [ 48 | 'class' => $this->cssClass, 49 | 'href' => $this->imageLinker->buildUrl( $title ) 50 | ], 51 | $fileName 52 | ); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/ImageLinker.php: -------------------------------------------------------------------------------- 1 | 120, 1 => 240, ...] 41 | * @param string $languageCode 42 | * @param ImageLinker $imageLinker 43 | * @param string $captionCssClass 44 | * @throws \MWException 45 | */ 46 | public function __construct( 47 | ParserOptions $parserOptions, 48 | array $thumbLimits, 49 | string $languageCode, 50 | ImageLinker $imageLinker, 51 | string $captionCssClass 52 | ) { 53 | $this->language = MediaWikiServices::getInstance()->getContentLanguage(); 54 | $this->parserOptions = $parserOptions; 55 | $this->thumbLimits = $thumbLimits; 56 | $this->imageLinker = $imageLinker; 57 | $this->captionCssClass = $captionCssClass; 58 | } 59 | 60 | /** 61 | * @see ValueFormatter::format 62 | * 63 | * Formats the given commons file name as an HTML image gallery. 64 | * 65 | * @param StringValue $value The commons file name 66 | * 67 | * @throws InvalidArgumentException 68 | * @return string HTML 69 | */ 70 | public function format( $value ) { 71 | if ( !( $value instanceof StringValue ) ) { 72 | throw new InvalidArgumentException( 'Data value type mismatch. Expected a StringValue.' ); 73 | } 74 | 75 | $fileName = $value->getValue(); 76 | // We cannot use makeTitle because it does not secureAndSplit() 77 | $title = Title::makeTitleSafe( NS_FILE, $fileName ); 78 | if ( $title === null ) { 79 | return htmlspecialchars( $fileName ); 80 | } 81 | 82 | $transformOptions = [ 83 | 'width' => $this->getThumbWidth( $this->parserOptions->getThumbSize() ), 84 | 'height' => 1000 85 | ]; 86 | 87 | $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); 88 | 89 | $file = $repoGroup->findFile( $fileName ); 90 | if ( !$file instanceof File ) { 91 | return $this->getCaptionHtml( $title ); 92 | } 93 | $thumb = $file->transform( $transformOptions ); 94 | if ( !$thumb ) { 95 | return $this->getCaptionHtml( $title ); 96 | } 97 | 98 | Linker::processResponsiveImages( $file, $thumb, $transformOptions ); 99 | 100 | return $this->wrapThumb( $title, $thumb->toHtml() ) . $this->getCaptionHtml( $title, $file ); 101 | } 102 | 103 | private function getThumbWidth( int $thumbSize ): int { 104 | return $this->thumbLimits[$thumbSize] ?? self::FALLBACK_THUMBNAIL_WIDTH; 105 | } 106 | 107 | private function wrapThumb( Title $title, string $thumbHtml ): string { 108 | $attributes = [ 109 | 'class' => 'image', 110 | 'href' => $this->imageLinker->buildUrl( $title ) 111 | ]; 112 | 113 | return Html::rawElement( 114 | 'div', 115 | [ 'class' => 'thumb' ], 116 | Html::rawElement( 'a', $attributes, $thumbHtml ) 117 | ); 118 | } 119 | 120 | private function getCaptionHtml( Title $title, ?File $file = null ): string { 121 | $attributes = [ 122 | 'href' => $this->imageLinker->buildUrl( $title ) 123 | ]; 124 | $innerHtml = Html::element( 'a', $attributes, $title->getText() ); 125 | 126 | if ( $file ) { 127 | $innerHtml .= Html::element( 'br' ) . $this->getFileMetaHtml( $file ); 128 | } 129 | 130 | return Html::rawElement( 131 | 'div', 132 | [ 'class' => $this->captionCssClass ], 133 | $innerHtml 134 | ); 135 | } 136 | 137 | private function getFileMetaHtml( File $file ): string { 138 | return $this->language->semicolonList( [ 139 | $file->getDimensionsString(), 140 | htmlspecialchars( $this->language->formatSize( (int)$file->getSize() ) ) 141 | ] ); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/Services/LocalImageLinker.php: -------------------------------------------------------------------------------- 1 | getFullURL(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Services/LocalMediaRdfBuilder.php: -------------------------------------------------------------------------------- 1 | titleFactory = $titleFactory; 17 | } 18 | 19 | /** 20 | * @param DataValue $value 21 | * 22 | * @return string the object URI 23 | */ 24 | protected function getValueUri( DataValue $value ) { 25 | return $this->titleFactory->newFromText( $value->getValue(), NS_FILE )->getFullURL(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/WikibaseLocalMedia.php: -------------------------------------------------------------------------------- 1 | buildMediaValidators( 'doNotCheckExistence' ); 40 | } 41 | 42 | public function getFormatterBuilder(): FormatterBuilder { 43 | return new FormatterBuilder( 44 | WikibaseRepo::getDefaultValueFormatterBuilders(), 45 | $GLOBALS['wgThumbLimits'] 46 | ); 47 | } 48 | 49 | public function getRdfBuilder(): LocalMediaRdfBuilder { 50 | return new LocalMediaRdfBuilder( MediaWikiServices::getInstance()->getTitleFactory() ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/js/jquery.ui.mediasuggester.tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license GPL-2.0+ 3 | */ 4 | ( function () { 5 | 'use strict'; 6 | 7 | /** 8 | * @return {jQuery} 9 | */ 10 | var newTestSuggester = function( mockSearchResult ) { 11 | var options = { 12 | ajax: function( options ) { 13 | var response = { query: { search: mockSearchResult || [] } }; 14 | 15 | // This uses the search results array as a spy, and appends _requestTerm 16 | response.query.search._requestTerm = options.data.srsearch; 17 | 18 | return $.Deferred().resolve( response ).promise(); 19 | }, 20 | apiUrl: 'can not be empty' 21 | }; 22 | 23 | return $( '' ) 24 | .addClass( 'test_suggester' ) 25 | .appendTo( 'body' ) 26 | .mediasuggester( options ); 27 | }; 28 | 29 | QUnit.module( 'jquery.ui.mediasuggester', { 30 | afterEach: function() { 31 | var $suggester = $( '.test_suggester' ), 32 | suggester = $suggester.data( 'mediasuggester' ); 33 | if ( suggester ) { 34 | suggester.destroy(); 35 | } 36 | $suggester.remove(); 37 | } 38 | } ); 39 | 40 | QUnit.test( 'Create', function( assert ) { 41 | var $suggester = newTestSuggester(); 42 | 43 | assert.ok( 44 | $suggester.data( 'mediasuggester' ) instanceof $.ui.mediasuggester, 45 | 'Instantiated media suggester.' 46 | ); 47 | } ); 48 | 49 | QUnit.test( 'search integration', function( assert ) { 50 | var $suggester = newTestSuggester(), 51 | suggester = $suggester.data( 'mediasuggester' ), 52 | input = 'Bar', 53 | done = assert.async(); 54 | 55 | $suggester.val( input ); 56 | suggester.search().done( function( suggestions, term ) { 57 | assert.strictEqual( suggestions._requestTerm, 'Bar' ); 58 | assert.strictEqual( term, input ); 59 | 60 | done(); 61 | } ); 62 | } ); 63 | 64 | QUnit.test( 'put matching file name on top of result list', function( assert ) { 65 | var $suggester = newTestSuggester( [ 66 | { title: 'File:mockResult_a.jpg' }, 67 | { title: 'File:mockResult_b.jpg' }, 68 | { title: 'File:mockResult_c.jpg' } 69 | ] ), 70 | suggester = $suggester.data( 'mediasuggester' ), 71 | input = 'mockResult_b.jpg', 72 | done = assert.async(); 73 | 74 | $suggester.val( input ); 75 | suggester.search().done( function( suggestions, term ) { 76 | assert.strictEqual( suggestions[0].title, 'File:mockResult_b.jpg' ); 77 | assert.strictEqual( suggestions[2].title, 'File:mockResult_c.jpg' ); 78 | done(); 79 | } ); 80 | } ); 81 | 82 | }() ); 83 | -------------------------------------------------------------------------------- /tests/php/Integration/FormattingTest.php: -------------------------------------------------------------------------------- 1 | newFormatterForFormat( $format ); 25 | 26 | $this->assertEquals( 27 | $expected, 28 | $formatter->format( new StringValue( 'Jonas-revenge.png' ) ) 29 | ); 30 | } 31 | 32 | public function formattingProvider() { 33 | yield [ SnakFormatter::FORMAT_WIKI, '[[File:Jonas-revenge.png|frameless]]' ]; 34 | yield [ SnakFormatter::FORMAT_PLAIN, 'Jonas-revenge.png' ]; 35 | } 36 | 37 | private function newFormatterForFormat( string $format ): ValueFormatter { 38 | return WikibaseLocalMedia::getGlobalInstance()->getFormatterBuilder()->newFormatter( 39 | $format, 40 | $this->newOptions() 41 | ); 42 | } 43 | 44 | private function newOptions(): FormatterOptions { 45 | return new FormatterOptions( [ 'lang' => 'en' ] ); 46 | } 47 | 48 | public function testHtmlFormat() { 49 | if ( !method_exists( $this, 'assertStringContainsString' ) ) { 50 | $this->markTestSkipped(); 51 | } 52 | 53 | $formatter = $this->newFormatterForFormat( SnakFormatter::FORMAT_HTML_VERBOSE ); 54 | 55 | $html = $formatter->format( new StringValue( 'ValidImageThatDoesNotExist.png' ) ); 56 | 57 | $this->assertStringContainsString( '
', $html ); 58 | $this->assertStringContainsString( 'href="', $html ); 59 | $this->assertStringContainsString( 'ValidImageThatDoesNotExist.png', $html ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tests/php/Integration/ValidationTest.php: -------------------------------------------------------------------------------- 1 | getValueValidators(); 18 | 19 | $this->assertNotEmpty( $validators ); 20 | 21 | foreach ( $validators as $validator ) { 22 | $this->assertCount( 0, $validator->validate( new StringValue( 'Valid-value.png' ) )->getErrors() ); 23 | } 24 | } 25 | 26 | public function testValidationFails() { 27 | $validators = WikibaseLocalMedia::getGlobalInstance()->getValueValidators(); 28 | 29 | $this->assertNotEmpty( $validators ); 30 | 31 | $errors = []; 32 | 33 | foreach ( $validators as $validator ) { 34 | $errors = array_merge( $errors, $validator->validate( new StringValue( 'Invalid-value' ) )->getErrors() ); 35 | } 36 | 37 | $this->assertNotEmpty( $errors ); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/php/Unit/ImageLinkFormatterTest.php: -------------------------------------------------------------------------------- 1 | assertStringContainsString( 21 | 'class="kittens"', 22 | $formatter->format( new StringValue( 'MyImage.png' ) ) 23 | ); 24 | } else { 25 | $this->assertContains( 26 | 'class="kittens"', 27 | $formatter->format( new StringValue( 'MyImage.png' ) ) 28 | ); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/php/Unit/LocalImageLinkerTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 22 | $title->getFullURL(), 23 | ( new LocalImageLinker() )->buildUrl( $title ) 24 | ); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /tests/php/Unit/LocalMediaRdfBuilderTest.php: -------------------------------------------------------------------------------- 1 | getTitleFactory() ); 27 | 28 | $writer = new NTriplesRdfWriter(); 29 | $writer->prefix( 'www', "http://www/" ); 30 | $writer->prefix( 'acme', "http://acme/" ); 31 | 32 | $writer->start(); 33 | $writer->about( 'www', 'Q42' ); 34 | 35 | $snak = new PropertyValueSnak( 36 | // Wikibase 1.37+ 37 | class_exists( NumericPropertyId::class ) ? 38 | new NumericPropertyId( 'P1' ) : 39 | new PropertyId( 'P1' ), 40 | new StringValue( 'Bunny.jpg' ) 41 | ); 42 | 43 | $builder->addValue( $writer, 'acme', 'testing', 'DUMMY', '', $snak ); 44 | $rdf = $writer->drain(); 45 | 46 | $this->assertStringContainsString( 'File:Bunny.jpg', $rdf ); 47 | $this->assertStringContainsString( $GLOBALS['wgServer'], $rdf ); 48 | } 49 | 50 | public function testGetRdfBuilder() { 51 | WikibaseLocalMedia::getGlobalInstance()->getRdfBuilder(); 52 | $this->assertTrue( true ); 53 | } 54 | 55 | } 56 | --------------------------------------------------------------------------------