├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── installMediaWiki.sh ├── .gitignore ├── COPYING.txt ├── Gruntfile.js ├── README.md ├── build └── release │ └── build_tarballs.sh ├── composer.json ├── docs └── ISSUE_TEMPLATE.md ├── extension.json ├── i18n ├── ar.json ├── bn.json ├── cs.json ├── de.json ├── el.json ├── en.json ├── es-formal.json ├── es.json ├── fr.json ├── he.json ├── ia.json ├── ko.json ├── lb.json ├── lfn.json ├── mk.json ├── nb.json ├── nl.json ├── pl.json ├── pt-br.json ├── pt.json ├── qqq.json ├── roa-tara.json ├── ru.json ├── sl.json ├── sr-ec.json ├── tr.json ├── uk.json ├── vi.json ├── zh-hans.json ├── zh-hant.json └── zh-hk.json ├── package.json ├── phpunit.xml.dist ├── release-notes.md ├── res ├── ext.SimpleBatchUpload.css ├── ext.SimpleBatchUpload.js ├── jquery.fileupload.css └── jquery.fileupload.js ├── src ├── ParameterProvider.php ├── SimpleBatchUpload.alias.php ├── SimpleBatchUpload.magic.php ├── SimpleBatchUpload.php ├── SpecialBatchUpload.php └── UploadButtonRenderer.php └── tests └── phpunit └── SimpleBatchUploadTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: JeroenDeDauw # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 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/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "composer" 6 | directory: "/" # Location of package manifests 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.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 | continue-on-error: ${{ matrix.experimental }} 11 | 12 | strategy: 13 | matrix: 14 | include: 15 | - mw: 'REL1_35' 16 | php: '8.0' 17 | experimental: false 18 | - mw: 'REL1_39' 19 | php: '8.1' 20 | experimental: false 21 | - mw: 'REL1_40' 22 | php: '8.2' 23 | experimental: false 24 | - mw: 'REL1_41' 25 | php: '8.2' 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: 44 | - name: Setup PHP 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | extensions: mbstring, intl 49 | tools: composer 50 | 51 | - name: Cache MediaWiki 52 | id: cache-mediawiki 53 | uses: actions/cache@v4 54 | with: 55 | path: | 56 | mediawiki 57 | !mediawiki/extensions/ 58 | !mediawiki/vendor/ 59 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4 60 | 61 | - name: Cache Composer cache 62 | uses: actions/cache@v4 63 | with: 64 | path: ~/.composer/cache 65 | key: composer-php${{ matrix.php }} 66 | 67 | - uses: actions/checkout@v4 68 | with: 69 | path: EarlyCopy 70 | 71 | - name: Install MediaWiki 72 | if: steps.cache-mediawiki.outputs.cache-hit != 'true' 73 | working-directory: ~ 74 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} SimpleBatchUpload 75 | 76 | - uses: actions/checkout@v4 77 | with: 78 | path: mediawiki/extensions/SimpleBatchUpload 79 | 80 | - run: composer update 81 | 82 | - name: Run update.php 83 | run: php maintenance/update.php --quick 84 | 85 | - name: Run PHPUnit 86 | run: php tests/phpunit/phpunit.php -c extensions/SimpleBatchUpload/ 87 | 88 | - name: Run PHPUnit with code coverage 89 | run: php tests/phpunit/phpunit.php -c extensions/SimpleBatchUpload/ --coverage-clover coverage.xml 90 | 91 | - name: Upload code coverage 92 | run: bash <(curl -s https://codecov.io/bash) 93 | if: github.ref == 'refs/heads/master' 94 | -------------------------------------------------------------------------------- /.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( "'$EXTENSION_NAME'" );' >> LocalSettings.php 23 | 24 | cat <> composer.local.json 25 | { 26 | "extra": { 27 | "merge-plugin": { 28 | "merge-dev": true, 29 | "include": [ 30 | "extensions/$EXTENSION_NAME/composer.json" 31 | ] 32 | } 33 | } 34 | } 35 | EOT 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -*- mode: gitignore; -*- 2 | *~ 3 | \#*\# 4 | .\#* 5 | node_modules 6 | vendor 7 | composer.lock 8 | package-lock.json -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | The license text below "----" applies to all files within this distribution, 2 | other than those that are in a directory which contains files named "LICENSE" 3 | or "COPYING", or a subdirectory thereof. For those files, the license text 4 | contained in said file overrides any license information contained in 5 | directories of smaller depth. Alternative licenses are typically used for 6 | software that is provided by external parties, and merely packaged with this 7 | software for convenience. 8 | 9 | ---- 10 | 11 | GNU GENERAL PUBLIC LICENSE 12 | Version 2, June 1991 13 | 14 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 15 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 | Everyone is permitted to copy and distribute verbatim copies 17 | of this license document, but changing it is not allowed. 18 | 19 | Preamble 20 | 21 | The licenses for most software are designed to take away your 22 | freedom to share and change it. By contrast, the GNU General Public 23 | License is intended to guarantee your freedom to share and change free 24 | software--to make sure the software is free for all its users. This 25 | General Public License applies to most of the Free Software 26 | Foundation's software and to any other program whose authors commit to 27 | using it. (Some other Free Software Foundation software is covered by 28 | the GNU Library General Public License instead.) You can apply it to 29 | your programs, too. 30 | 31 | When we speak of free software, we are referring to freedom, not 32 | price. Our General Public Licenses are designed to make sure that you 33 | have the freedom to distribute copies of free software (and charge for 34 | this service if you wish), that you receive source code or can get it 35 | if you want it, that you can change the software or use pieces of it 36 | in new free programs; and that you know you can do these things. 37 | 38 | To protect your rights, we need to make restrictions that forbid 39 | anyone to deny you these rights or to ask you to surrender the rights. 40 | These restrictions translate to certain responsibilities for you if you 41 | distribute copies of the software, or if you modify it. 42 | 43 | For example, if you distribute copies of such a program, whether 44 | gratis or for a fee, you must give the recipients all the rights that 45 | you have. You must make sure that they, too, receive or can get the 46 | source code. And you must show them these terms so they know their 47 | rights. 48 | 49 | We protect your rights with two steps: (1) copyright the software, and 50 | (2) offer you this license which gives you legal permission to copy, 51 | distribute and/or modify the software. 52 | 53 | Also, for each author's protection and ours, we want to make certain 54 | that everyone understands that there is no warranty for this free 55 | software. If the software is modified by someone else and passed on, we 56 | want its recipients to know that what they have is not the original, so 57 | that any problems introduced by others will not reflect on the original 58 | authors' reputations. 59 | 60 | Finally, any free program is threatened constantly by software 61 | patents. We wish to avoid the danger that redistributors of a free 62 | program will individually obtain patent licenses, in effect making the 63 | program proprietary. To prevent this, we have made it clear that any 64 | patent must be licensed for everyone's free use or not licensed at all. 65 | 66 | The precise terms and conditions for copying, distribution and 67 | modification follow. 68 | 69 | GNU GENERAL PUBLIC LICENSE 70 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 71 | 72 | 0. This License applies to any program or other work which contains 73 | a notice placed by the copyright holder saying it may be distributed 74 | under the terms of this General Public License. The "Program", below, 75 | refers to any such program or work, and a "work based on the Program" 76 | means either the Program or any derivative work under copyright law: 77 | that is to say, a work containing the Program or a portion of it, 78 | either verbatim or with modifications and/or translated into another 79 | language. (Hereinafter, translation is included without limitation in 80 | the term "modification".) Each licensee is addressed as "you". 81 | 82 | Activities other than copying, distribution and modification are not 83 | covered by this License; they are outside its scope. The act of 84 | running the Program is not restricted, and the output from the Program 85 | is covered only if its contents constitute a work based on the 86 | Program (independent of having been made by running the Program). 87 | Whether that is true depends on what the Program does. 88 | 89 | 1. You may copy and distribute verbatim copies of the Program's 90 | source code as you receive it, in any medium, provided that you 91 | conspicuously and appropriately publish on each copy an appropriate 92 | copyright notice and disclaimer of warranty; keep intact all the 93 | notices that refer to this License and to the absence of any warranty; 94 | and give any other recipients of the Program a copy of this License 95 | along with the Program. 96 | 97 | You may charge a fee for the physical act of transferring a copy, and 98 | you may at your option offer warranty protection in exchange for a fee. 99 | 100 | 2. You may modify your copy or copies of the Program or any portion 101 | of it, thus forming a work based on the Program, and copy and 102 | distribute such modifications or work under the terms of Section 1 103 | above, provided that you also meet all of these conditions: 104 | 105 | a) You must cause the modified files to carry prominent notices 106 | stating that you changed the files and the date of any change. 107 | 108 | b) You must cause any work that you distribute or publish, that in 109 | whole or in part contains or is derived from the Program or any 110 | part thereof, to be licensed as a whole at no charge to all third 111 | parties under the terms of this License. 112 | 113 | c) If the modified program normally reads commands interactively 114 | when run, you must cause it, when started running for such 115 | interactive use in the most ordinary way, to print or display an 116 | announcement including an appropriate copyright notice and a 117 | notice that there is no warranty (or else, saying that you provide 118 | a warranty) and that users may redistribute the program under 119 | these conditions, and telling the user how to view a copy of this 120 | License. (Exception: if the Program itself is interactive but 121 | does not normally print such an announcement, your work based on 122 | the Program is not required to print an announcement.) 123 | 124 | These requirements apply to the modified work as a whole. If 125 | identifiable sections of that work are not derived from the Program, 126 | and can be reasonably considered independent and separate works in 127 | themselves, then this License, and its terms, do not apply to those 128 | sections when you distribute them as separate works. But when you 129 | distribute the same sections as part of a whole which is a work based 130 | on the Program, the distribution of the whole must be on the terms of 131 | this License, whose permissions for other licensees extend to the 132 | entire whole, and thus to each and every part regardless of who wrote it. 133 | 134 | Thus, it is not the intent of this section to claim rights or contest 135 | your rights to work written entirely by you; rather, the intent is to 136 | exercise the right to control the distribution of derivative or 137 | collective works based on the Program. 138 | 139 | In addition, mere aggregation of another work not based on the Program 140 | with the Program (or with a work based on the Program) on a volume of 141 | a storage or distribution medium does not bring the other work under 142 | the scope of this License. 143 | 144 | 3. You may copy and distribute the Program (or a work based on it, 145 | under Section 2) in object code or executable form under the terms of 146 | Sections 1 and 2 above provided that you also do one of the following: 147 | 148 | a) Accompany it with the complete corresponding machine-readable 149 | source code, which must be distributed under the terms of Sections 150 | 1 and 2 above on a medium customarily used for software interchange; or, 151 | 152 | b) Accompany it with a written offer, valid for at least three 153 | years, to give any third party, for a charge no more than your 154 | cost of physically performing source distribution, a complete 155 | machine-readable copy of the corresponding source code, to be 156 | distributed under the terms of Sections 1 and 2 above on a medium 157 | customarily used for software interchange; or, 158 | 159 | c) Accompany it with the information you received as to the offer 160 | to distribute corresponding source code. (This alternative is 161 | allowed only for noncommercial distribution and only if you 162 | received the program in object code or executable form with such 163 | an offer, in accord with Subsection b above.) 164 | 165 | The source code for a work means the preferred form of the work for 166 | making modifications to it. For an executable work, complete source 167 | code means all the source code for all modules it contains, plus any 168 | associated interface definition files, plus the scripts used to 169 | control compilation and installation of the executable. However, as a 170 | special exception, the source code distributed need not include 171 | anything that is normally distributed (in either source or binary 172 | form) with the major components (compiler, kernel, and so on) of the 173 | operating system on which the executable runs, unless that component 174 | itself accompanies the executable. 175 | 176 | If distribution of executable or object code is made by offering 177 | access to copy from a designated place, then offering equivalent 178 | access to copy the source code from the same place counts as 179 | distribution of the source code, even though third parties are not 180 | compelled to copy the source along with the object code. 181 | 182 | 4. You may not copy, modify, sublicense, or distribute the Program 183 | except as expressly provided under this License. Any attempt 184 | otherwise to copy, modify, sublicense or distribute the Program is 185 | void, and will automatically terminate your rights under this License. 186 | However, parties who have received copies, or rights, from you under 187 | this License will not have their licenses terminated so long as such 188 | parties remain in full compliance. 189 | 190 | 5. You are not required to accept this License, since you have not 191 | signed it. However, nothing else grants you permission to modify or 192 | distribute the Program or its derivative works. These actions are 193 | prohibited by law if you do not accept this License. Therefore, by 194 | modifying or distributing the Program (or any work based on the 195 | Program), you indicate your acceptance of this License to do so, and 196 | all its terms and conditions for copying, distributing or modifying 197 | the Program or works based on it. 198 | 199 | 6. Each time you redistribute the Program (or any work based on the 200 | Program), the recipient automatically receives a license from the 201 | original licensor to copy, distribute or modify the Program subject to 202 | these terms and conditions. You may not impose any further 203 | restrictions on the recipients' exercise of the rights granted herein. 204 | You are not responsible for enforcing compliance by third parties to 205 | this License. 206 | 207 | 7. If, as a consequence of a court judgment or allegation of patent 208 | infringement or for any other reason (not limited to patent issues), 209 | conditions are imposed on you (whether by court order, agreement or 210 | otherwise) that contradict the conditions of this License, they do not 211 | excuse you from the conditions of this License. If you cannot 212 | distribute so as to satisfy simultaneously your obligations under this 213 | License and any other pertinent obligations, then as a consequence you 214 | may not distribute the Program at all. For example, if a patent 215 | license would not permit royalty-free redistribution of the Program by 216 | all those who receive copies directly or indirectly through you, then 217 | the only way you could satisfy both it and this License would be to 218 | refrain entirely from distribution of the Program. 219 | 220 | If any portion of this section is held invalid or unenforceable under 221 | any particular circumstance, the balance of the section is intended to 222 | apply and the section as a whole is intended to apply in other 223 | circumstances. 224 | 225 | It is not the purpose of this section to induce you to infringe any 226 | patents or other property right claims or to contest validity of any 227 | such claims; this section has the sole purpose of protecting the 228 | integrity of the free software distribution system, which is 229 | implemented by public license practices. Many people have made 230 | generous contributions to the wide range of software distributed 231 | through that system in reliance on consistent application of that 232 | system; it is up to the author/donor to decide if he or she is willing 233 | to distribute software through any other system and a licensee cannot 234 | impose that choice. 235 | 236 | This section is intended to make thoroughly clear what is believed to 237 | be a consequence of the rest of this License. 238 | 239 | 8. If the distribution and/or use of the Program is restricted in 240 | certain countries either by patents or by copyrighted interfaces, the 241 | original copyright holder who places the Program under this License 242 | may add an explicit geographical distribution limitation excluding 243 | those countries, so that distribution is permitted only in or among 244 | countries not thus excluded. In such case, this License incorporates 245 | the limitation as if written in the body of this License. 246 | 247 | 9. The Free Software Foundation may publish revised and/or new versions 248 | of the General Public License from time to time. Such new versions will 249 | be similar in spirit to the present version, but may differ in detail to 250 | address new problems or concerns. 251 | 252 | Each version is given a distinguishing version number. If the Program 253 | specifies a version number of this License which applies to it and "any 254 | later version", you have the option of following the terms and conditions 255 | either of that version or of any later version published by the Free 256 | Software Foundation. If the Program does not specify a version number of 257 | this License, you may choose any version ever published by the Free Software 258 | Foundation. 259 | 260 | 10. If you wish to incorporate parts of the Program into other free 261 | programs whose distribution conditions are different, write to the author 262 | to ask for permission. For software which is copyrighted by the Free 263 | Software Foundation, write to the Free Software Foundation; we sometimes 264 | make exceptions for this. Our decision will be guided by the two goals 265 | of preserving the free status of all derivatives of our free software and 266 | of promoting the sharing and reuse of software generally. 267 | 268 | NO WARRANTY 269 | 270 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 271 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 272 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 273 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 274 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 275 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 276 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 277 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 278 | REPAIR OR CORRECTION. 279 | 280 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 281 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 282 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 283 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 284 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 285 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 286 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 287 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 288 | POSSIBILITY OF SUCH DAMAGES. 289 | 290 | END OF TERMS AND CONDITIONS 291 | 292 | How to Apply These Terms to Your New Programs 293 | 294 | If you develop a new program, and you want it to be of the greatest 295 | possible use to the public, the best way to achieve this is to make it 296 | free software which everyone can redistribute and change under these terms. 297 | 298 | To do so, attach the following notices to the program. It is safest 299 | to attach them to the start of each source file to most effectively 300 | convey the exclusion of warranty; and each file should have at least 301 | the "copyright" line and a pointer to where the full notice is found. 302 | 303 | 304 | Copyright (C) 305 | 306 | This program is free software; you can redistribute it and/or modify 307 | it under the terms of the GNU General Public License as published by 308 | the Free Software Foundation; either version 2 of the License, or 309 | (at your option) any later version. 310 | 311 | This program is distributed in the hope that it will be useful, 312 | but WITHOUT ANY WARRANTY; without even the implied warranty of 313 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 314 | GNU General Public License for more details. 315 | 316 | You should have received a copy of the GNU General Public License 317 | along with this program; if not, write to the Free Software 318 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 319 | 320 | 321 | Also add information on how to contact you by electronic and paper mail. 322 | 323 | If the program is interactive, make it output a short notice like this 324 | when it starts in an interactive mode: 325 | 326 | Gnomovision version 69, Copyright (C) year name of author 327 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 328 | This is free software, and you are welcome to redistribute it 329 | under certain conditions; type `show c' for details. 330 | 331 | The hypothetical commands `show w' and `show c' should show the appropriate 332 | parts of the General Public License. Of course, the commands you use may 333 | be called something other than `show w' and `show c'; they could even be 334 | mouse-clicks or menu items--whatever suits your program. 335 | 336 | You should also get your employer (if you work as a programmer) or your 337 | school, if any, to sign a "copyright disclaimer" for the program, if 338 | necessary. Here is a sample; alter the names: 339 | 340 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 341 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 342 | 343 | , 1 April 1989 344 | Ty Coon, President of Vice 345 | 346 | This General Public License does not permit incorporating your program into 347 | proprietary programs. If your program is a subroutine library, you may 348 | consider it more useful to permit linking proprietary applications with the 349 | library. If this is what you want to do, use the GNU Library General 350 | Public License instead of this License. 351 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | module.exports = function ( grunt ) { 3 | 4 | 'use strict'; 5 | 6 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 7 | grunt.loadNpmTasks( 'grunt-jsonlint' ); 8 | grunt.loadNpmTasks( 'grunt-banana-checker' ); 9 | 10 | grunt.initConfig( { 11 | jshint: { 12 | options: { 13 | // Enforcing 14 | "bitwise": true, 15 | "curly": true, 16 | "eqeqeq": true, 17 | "freeze": true, 18 | "latedef": "nofunc", 19 | "noarg": true, 20 | "nonew": true, 21 | "undef": true, 22 | "unused": true, 23 | "strict": true, 24 | 25 | // ECMAScript version 26 | "esversion": 3, 27 | 28 | // Environment 29 | "browser": true, 30 | "jquery": true, 31 | 32 | // map of global variables, with keys as names and a boolean value to determine if they are assignable 33 | "globals": { 34 | "mediaWiki": false 35 | }, 36 | 37 | "ignores": [] 38 | }, 39 | all: [ 40 | '**/*.js', 41 | '!node_modules/**' 42 | ] 43 | }, 44 | banana: { 45 | all: 'i18n/' 46 | }, 47 | jsonlint: { 48 | all: [ 49 | '**/*.json', 50 | '!node_modules/**' 51 | ] 52 | } 53 | } ); 54 | 55 | grunt.registerTask( 'lint', [ 'jshint', 'jsonlint', 'banana' ] ); 56 | grunt.registerTask( 'test', [ 'lint' ] ); 57 | grunt.registerTask( 'default', 'test' ); 58 | }; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleBatchUpload 2 | 3 | [![GitHub Workflow Status](https://github.com/ProfessionalWiki/SimpleBatchUpload/actions/workflows/ci.yml/badge.svg)](https://github.com/ProfessionalWiki/SimpleBatchUpload/actions?query=workflow%3ACI) 4 | [![Latest Stable Version](https://poser.pugx.org/mediawiki/simple-batch-upload/v/stable)](https://packagist.org/packages/mediawiki/simple-batch-upload) 5 | [![Packagist download count](https://poser.pugx.org/mediawiki/simple-batch-upload/downloads)](https://packagist.org/packages/mediawiki/simple-batch-upload) 6 | 7 | The [SimpleBatchUpload][mw-simple-batch-upload] extension provides basic, 8 | no-frills uploading of multiple files to MediaWiki. 9 | 10 | It is maintained by [Professional.Wiki](https://professional.wiki/). 11 | [Contact us](https://professional.wiki/en/contact) for commercial support or development work. 12 | 13 | ## Requirements 14 | 15 | - PHP 8.0 or later 16 | - MediaWiki 1.35 or later 17 | 18 | ## Installation 19 | 20 | ### Composer 21 | ```sh 22 | COMPOSER=composer.local.json composer require --no-update mediawiki/simple-batch-upload:^2.0 23 | ``` 24 | ```sh 25 | composer update mediawiki/simple-batch-upload --no-dev -o 26 | ``` 27 | 28 | ### Manual installation 29 | 30 | [Download](https://github.com/ProfessionalWiki/SimpleBatchUpload/releases) and place the files in a directory called `SimpleBatchUpload` in your `extensions/` folder. 31 | 32 | 33 | Enable the extension by adding the following to your LocalSettings.php: 34 | ```php 35 | wfLoadExtension( 'SimpleBatchUpload' ); 36 | ``` 37 | 38 | **Note:** To use the extension the user needs the [`writeapi`][writeapi] right. This is the 39 | default MediaWiki setting for registered users, but it may have been changed 40 | during the configuration of the wiki. 41 | 42 | ## Usage 43 | 44 | There are four ways to upload files using this extension: 45 | * Go to _Special:BatchUpload_ to get a plain upload page 46 | * Go to _Special:BatchUpload/Foo_ to get an upload page that sets `{{Foo}}` as 47 | the wikitext of the uploaded file's page 48 | * Add `{{#batchupload:}}` to any wikipage to get a simple upload button 49 | * Add `{{#batchupload:Foo|Bar|Baz}}` to any wikipage to get an upload button 50 | that sets `{{Foo|Bar|Baz}}` as the wikitext of the uploaded file's page 51 | 52 | ## Customization 53 | 54 | It is possible to specify dedicated parameter sets for the upload of specific 55 | file types by editing the _MediaWiki:Simplebatchupload-parameters_ page. Each 56 | line of that page is considered as one set of parameters. 57 | 58 | Available parameters are: 59 | * Name of template to be stored as text on initial upload 60 | * Upload comment 61 | * Title line of the Special:BatchUpload page 62 | 63 | Parameters should be separated by pipes (|). 64 | 65 | The line to be used is selected by appending the name of the template as the 66 | subpage to the URL of the Special:BatchUpload page. 67 | 68 | __Example:__ 69 | 70 | Consider the parameter line 71 | ``` 72 | Pics | These pics were uploaded using [[mw:Extension:SimpleBatchUpload{{!}}SimpleBatchUpload]] | Upload some pics! 73 | ``` 74 | 75 | * This can be selected by going to _Special:BatchUpload/Pics_. 76 | * The title of this page will be _Upload some pics!_. 77 | * The comment for the upload will be _These pics were uploaded using [[mw:Extension:SimpleBatchUpload{{!}}SimpleBatchUpload]]_. 78 | * If a file with that name is uploaded for the first time it will have `{{Pics}}` as wikitext. 79 | 80 | ## Configuration 81 | 82 | Available configuration options: 83 | 84 | * `$wgSimpleBatchUploadMaxFilesPerBatch` - Array defining the maximum number of 85 | files that can be uploaded each time depending on the user group.
Default: 86 | ``` php 87 | $wgSimpleBatchUploadMaxFilesPerBatch = [ 88 | '*' => 1000, 89 | ]; 90 | ``` 91 | 92 | **Note:** Be aware that this is not the right setting to completely block file 93 | uploads! Users can still use the normal file upload or the MediaWiki API. See 94 | the paragraph on user permissions on 95 | [Configuring file uploads](https://www.mediawiki.org/wiki/Manual:Configuring_file_uploads#Upload_permissions) 96 | on mediawiki.org. 97 | 98 | 99 | ## License 100 | 101 | [GNU General Public License 2.0][license] or later 102 | 103 | [license]: https://www.gnu.org/copyleft/gpl.html 104 | [mw-simple-batch-upload]: https://www.mediawiki.org/wiki/Extension:SimpleBatchUpload 105 | [composer]: https://getcomposer.org/ 106 | [writeapi]: https://www.mediawiki.org/wiki/Manual:User_rights#List_of_permissions 107 | -------------------------------------------------------------------------------- /build/release/build_tarballs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Utility adapted from the tool for creating SMW tarballs 4 | # By Jeroen De Dauw < jeroendedauw@gmail.com > 5 | # 6 | # This script is not currently used. It is kept in case Composer-managed 7 | # dependencies are added again. 8 | # 9 | # @copyright (C) 2016-2019, Stephan Gambke 10 | # @license GNU General Public License, version 2 (or any later version) 11 | # 12 | # This software is free software; you can redistribute it and/or 13 | # modify it under the terms of the GNU General Public License 14 | # as published by the Free Software Foundation; either version 2 15 | # of the License, or (at your option) any later version. 16 | # This software is distributed in the hope that it will be useful, 17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | # GNU General Public License for more details. 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, see . 22 | 23 | # Parameters: 24 | # $1: version used for cloning and in the tarball name 25 | 26 | VERSION="$1" 27 | if [ "$VERSION" == "" ]; then 28 | VERSION="master" 29 | fi 30 | 31 | NAME="SimpleBatchUpload.$VERSION.with.dependencies" 32 | DIR="SimpleBatchUpload" 33 | 34 | BUILD_DIR="build-$VERSION" 35 | 36 | rm -rf "$BUILD_DIR" 37 | mkdir "$BUILD_DIR" 38 | cd "$BUILD_DIR" 39 | 40 | # composer create-project mediawiki/simple-batch-upload $DIR $COMPOSER_VERSION --stability dev --prefer-dist --no-dev --ignore-platform-reqs --no-install 41 | git clone https://github.com/ProfessionalWiki/SimpleBatchUpload.git "$DIR" 42 | git checkout "$VERSION" 43 | 44 | cd $DIR 45 | mv composer.json composer.json.orig 46 | sed s/\"mediawiki\\/mediawiki\":\ \"\>=1...\",//g composer.json.orig > composer.json 47 | composer install --prefer-dist --no-dev --ignore-platform-reqs --optimize-autoloader 48 | mv composer.json.orig composer.json 49 | rm -rf .git 50 | cd - 51 | 52 | zip -qro9 "$NAME.zip" $DIR 53 | tar -czf "$NAME.tar.gz" $DIR 54 | 55 | cd .. 56 | set -x 57 | ls -lap "$BUILD_DIR" 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediawiki/simple-batch-upload", 3 | "type": "mediawiki-extension", 4 | "description": "Basic, no-frills uploading of multiple files to MediaWiki", 5 | "keywords": [ 6 | "wiki", 7 | "MediaWiki", 8 | "extension" 9 | ], 10 | "homepage": "https://www.mediawiki.org/wiki/Extension:SimpleBatchUpload", 11 | "license": "GPL-2.0-or-later", 12 | "authors": [ 13 | { 14 | "name": "Stephan Gambke", 15 | "email": "s7eph4n@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "support": { 20 | "wiki": "https://www.mediawiki.org/wiki/Extension:SimpleBatchUpload", 21 | "forum": "https://www.mediawiki.org/wiki/Extension_talk:SimpleBatchUpload", 22 | "source": "https://github.com/ProfessionalWiki/SimpleBatchUpload", 23 | "issues": "https://github.com/ProfessionalWiki/SimpleBatchUpload/issues", 24 | "irc": "irc://libera.chat:6667/mediawiki" 25 | }, 26 | "require": { 27 | "php": ">=8.0", 28 | "composer/installers": "^2|^1.0.1" 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "2.x-dev" 33 | } 34 | }, 35 | "config": { 36 | "allow-plugins": { 37 | "composer/installers": true 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | 3 | - MW version: 4 | - PHP version: 5 | - DB (MySQL etc.): 6 | - SBL version: 7 | - Browser version: 8 | 9 | ### Issue 10 | 11 | ...context and general description... 12 | 13 | **Steps to reproduce:** ... 14 | 15 | **Expected result:** ... 16 | 17 | **Observed result:** ... 18 | 19 | **Stacktrace:** 20 | 21 | ``` 22 | ... 23 | ``` 24 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleBatchUpload", 3 | "version": "2.0.1", 4 | "author": [ 5 | "[https://www.mediawiki.org/wiki/User:F.trott Stephan Gambke]", 6 | "[https://professional.wiki/ Professional.Wiki]", 7 | "..." 8 | ], 9 | "url": "https://www.mediawiki.org/wiki/Extension:SimpleBatchUpload", 10 | "descriptionmsg": "simplebatchupload-desc", 11 | "namemsg": "simplebatchupload-name", 12 | "license-name": "GPL-2.0-or-later", 13 | "type": "specialpage", 14 | "requires": { 15 | "MediaWiki": ">=1.35" 16 | }, 17 | "MessagesDirs": { 18 | "SimpleBatchUpload": [ 19 | "i18n" 20 | ] 21 | }, 22 | "config": { 23 | "SimpleBatchUploadMaxFilesPerBatch": { 24 | "value":{ 25 | "*": 1000 26 | } 27 | } 28 | }, 29 | "AutoloadNamespaces": { 30 | "MediaWiki\\Extension\\SimpleBatchUpload\\": "src/" 31 | }, 32 | "TestAutoloadNamespaces": { 33 | "MediaWiki\\Extension\\SimpleBatchUpload\\Tests\\": "tests/phpunit/" 34 | }, 35 | "ExtensionMessagesFiles": { 36 | "SimpleBatchUploadAlias": "src/SimpleBatchUpload.alias.php", 37 | "SimpleBatchUploadMagic": "src/SimpleBatchUpload.magic.php" 38 | }, 39 | "SpecialPages": { 40 | "BatchUpload": "MediaWiki\\Extension\\SimpleBatchUpload\\SpecialBatchUpload" 41 | }, 42 | "Hooks": { 43 | "ParserFirstCallInit": "MediaWiki\\Extension\\SimpleBatchUpload\\SimpleBatchUpload::registerParserFunction", 44 | "MakeGlobalVariablesScript": "MediaWiki\\Extension\\SimpleBatchUpload\\SimpleBatchUpload::onMakeGlobalVariablesScript" 45 | }, 46 | "ResourceFileModulePaths": { 47 | "localBasePath": "res", 48 | "remoteExtPath": "SimpleBatchUpload/res" 49 | }, 50 | "ResourceModules": { 51 | "ext.SimpleBatchUpload.jquery-file-upload": { 52 | "scripts": [ 53 | "jquery.fileupload.js" 54 | ], 55 | "styles": [ 56 | "jquery.fileupload.css" 57 | ], 58 | "position": "top", 59 | "dependencies": [ 60 | "jquery.ui" 61 | ] 62 | }, 63 | "ext.SimpleBatchUpload": { 64 | "scripts": [ 65 | "ext.SimpleBatchUpload.js" 66 | ], 67 | "styles": [ 68 | "ext.SimpleBatchUpload.css" 69 | ], 70 | "position": "top", 71 | "dependencies": [ 72 | "ext.SimpleBatchUpload.jquery-file-upload", 73 | "mediawiki.Title", 74 | "mediawiki.jqueryMsg", 75 | "mediawiki.api" 76 | ], 77 | "messages": [ 78 | "simplebatchupload-comment", 79 | "simplebatchupload-max-files-alert" 80 | ] 81 | } 82 | }, 83 | "manifest_version": 2 84 | } 85 | -------------------------------------------------------------------------------- /i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Meno25" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "يسمح بتحميل دفعة بسيطة من الملفات", 8 | "batchupload": "تحميل ملفات متعددة", 9 | "simplebatchupload-buttonlabel": "حدد الملفات (أو أسقطها هنا) ...", 10 | "simplebatchupload-comment": "تم التحميل باستخدام [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "لا يمكنك تحميل أكثر من $1 {{PLURAL:$1| file | files}} في وقت واحد." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/bn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "আফতাবুজ্জামান" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "ফাইলগুলি সাধারণ ব্যাচে আপলোড করার জন্য মঞ্জুরি দেয়", 8 | "batchupload": "একাধিক ফাইল আপলোড করুন", 9 | "simplebatchupload-buttonlabel": "ফাইলগুলি নির্বাচন করুন (বা তাদের এখানে এনে ছেড়ে দিন)...", 10 | "simplebatchupload-comment": "[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]] দিয়ে আপলোডকৃত", 11 | "simplebatchupload-max-files-alert": "আপনি একবারে $1টির বেশি {{PLURAL:$1|ফাইল}} আপলোড করতে পারবেন না।" 12 | } 13 | -------------------------------------------------------------------------------- /i18n/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Matěj Suchánek", 5 | "Nesuprachy", 6 | "Petr Urbanec" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Umožňuje jednoduché načítání více souborů", 10 | "batchupload": "Načíst více souborů najednou", 11 | "simplebatchupload-buttonlabel": "Vyberte soubory (nebo je sem přetáhněte)...", 12 | "simplebatchupload-comment": "Načteno pomocí [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "Nemůžete najednou načíst více než $1 {{PLURAL:$1|soubor|soubory|souborů}}." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ameisenigel", 5 | "F.trott", 6 | "Kghbln" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Ermöglicht ein einfaches Hochladen mehrerer Dateien gleichzeitig", 10 | "batchupload": "Mehrere Dateien hochladen", 11 | "simplebatchupload-buttonlabel": "Dateien auswählen oder hierher ziehen...", 12 | "simplebatchupload-comment": "Hochgeladen mit [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "Mehr als {{PLURAL:$1|eine Datei|$1 Dateien}} können nicht auf einmal hochgeladen werden." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Geraki" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Επιτρέπει την απλή ομαδική επιφόρτωση αρχείων", 8 | "batchupload": "Επιφόρτωση πολλαπλών αρχείων", 9 | "simplebatchupload-buttonlabel": "Επιλέξτε αρχεία (ή ρίξτε τα εδώ)...", 10 | "simplebatchupload-comment": "Ανέβηκε με το [[mw:Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Δεν μπορείτε να ανεβάσετε περισσότερα από {{PLURAL:$1|αρχείο|αρχεία}} κάθε φορά." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "F.trott", 5 | "Kghbln" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "Allows for simple batch uploading of of files", 9 | "simplebatchupload-name": "SimpleBatchUpload", 10 | "batchupload": "Upload multiple files", 11 | "simplebatchupload-buttonlabel": "Select files (or drop them here)...", 12 | "simplebatchupload-comment": "Uploaded with [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "You cannot upload more than $1 {{PLURAL:$1|file|files}} at a time." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/es-formal.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Fitoschido" 5 | ] 6 | }, 7 | "simplebatchupload-buttonlabel": "Seleccione archivos (o colóquelos aquí)…" 8 | } 9 | -------------------------------------------------------------------------------- /i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Fitoschido", 5 | "MarcoAurelio", 6 | "No se" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Permite la carga por lotes sencilla de archivos", 10 | "batchupload": "Cargar varios archivos", 11 | "simplebatchupload-buttonlabel": "Selecciona archivos (o colócalos aquí)…", 12 | "simplebatchupload-comment": "Cargado mediante [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "No es posible cargar más de $1 {{PLURAL:$1|archivo|archivos}} a la vez." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Gomoko", 5 | "Wladek92" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "Permet un téléchargement simple en tâche de fond de fichiers", 9 | "batchupload": "Téléverser plusieurs fichiers", 10 | "simplebatchupload-buttonlabel": "Sélectionner les fichiers (ou les faire glisser ici)…", 11 | "simplebatchupload-comment": "Téléversé avec [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 12 | "simplebatchupload-max-files-alert": "Vous ne pouvez pas télécharger plus de $1 {{PLURAL:$1|fichier|fichiers}} à la fois." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Amire80", 5 | "YaronSh" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "אפשרות העלאה פשוטה של קבצים באצווה", 9 | "batchupload": "העלאת קבצים מרובים", 10 | "simplebatchupload-buttonlabel": "נא לבחור קבצים (או לגרור ולהשלליך אותם לכאן)…", 11 | "simplebatchupload-comment": "הועלה עם [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 12 | "simplebatchupload-max-files-alert": "אין לך אפשרות להעלות יותר {{PLURAL:$1|מקובץ אחד|מ־$1 קבצים}} בכל פעם." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/ia.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "McDutchie" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Permitte le incargamento simple in lot de files", 8 | "batchupload": "Incargar plure files", 9 | "simplebatchupload-buttonlabel": "Selige files (o depone los hic)…", 10 | "simplebatchupload-comment": "Incargate con [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Non es possibile incargar plus de $1 file{{PLURAL:$1||s}} al vice." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ykhwong" 5 | ] 6 | }, 7 | "batchupload": "여러 개의 파일 업로드", 8 | "simplebatchupload-buttonlabel": "파일 선택 (또는 여기에 끌어놓으세요)..." 9 | } 10 | -------------------------------------------------------------------------------- /i18n/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Robby" 5 | ] 6 | }, 7 | "batchupload": "Méi Fichieren eroplueden", 8 | "simplebatchupload-comment": "Eropgeluede mam [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 9 | "simplebatchupload-max-files-alert": "Dir kënnt net méi wéi {{PLURAL:$1|ee Fichier|$1 Fichiere}} mateneen eroplueden." 10 | } 11 | -------------------------------------------------------------------------------- /i18n/lfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Elefentiste" 5 | ] 6 | }, 7 | "batchupload": "Carga fixes multiple", 8 | "simplebatchupload-buttonlabel": "Eleje fixes (o pone los asi)...", 9 | "simplebatchupload-max-files-alert": "Tu no pote carga plu ca $1 {{PLURAL:$1|fix|fixes}} a un ves." 10 | } 11 | -------------------------------------------------------------------------------- /i18n/mk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Bjankuloski06" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Овозможува просто збирно подигање на податотеки", 8 | "batchupload": "Подигање на повеќе податотеки наеднаш", 9 | "simplebatchupload-buttonlabel": "Изберете податотеки (или пуштете ги овде)...", 10 | "simplebatchupload-comment": "Подигнато со [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|Просто збирно подигање]]", 11 | "simplebatchupload-max-files-alert": "Не можете наеднаш да подигате повеќе од {{PLURAL:$1|една податотека|$1 податотеки}}." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Jon Harald Søby" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Gjør det mulig å masseopplaste filer på en enkel måte", 8 | "batchupload": "Last opp flere filer", 9 | "simplebatchupload-buttonlabel": "Velg filer (eller slipp dem her) …", 10 | "simplebatchupload-comment": "Lastet opp med [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Du kan ikke laste opp mer enn {{PLURAL:$1|én fil|$1 filer}} av gangen." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Mainframe98", 5 | "Tkinkhorst", 6 | "Xbaked potatox" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Maakt uploaden van meerdere bestanden eenvoudig mogelijk", 10 | "batchupload": "Meerdere bestanden uploaden", 11 | "simplebatchupload-buttonlabel": "Selecteer bestanden (of sleep ze hier naar toe)...", 12 | "simplebatchupload-comment": "Geüpload met [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "U kunt niet meer dan $1 {{PLURAL:$1|bestand|bestanden}} tegelijkertijd uploaden." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Rail", 5 | "Railfail536", 6 | "WaldiSt" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Proste przesyłanie grup plików", 10 | "batchupload": "Prześlij wiele plików", 11 | "simplebatchupload-buttonlabel": "Wybierz pliki (lub upuść je tutaj)…", 12 | "simplebatchupload-comment": "Przesłano z\n[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "Nie możesz przesłać więcej niż $1 {{PLURAL:$1|pliku|plików}} na raz." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eduardo Addad de Oliveira", 5 | "Eduardoaddad", 6 | "Jhonnatanricardo" 7 | ] 8 | }, 9 | "simplebatchupload-desc": "Permite o carregamento em lote simples de arquivos", 10 | "batchupload": "Carregar vários arquivos", 11 | "simplebatchupload-buttonlabel": "Selecione os arquivos (ou solte-os aqui)...", 12 | "simplebatchupload-comment": "Carregado com [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 13 | "simplebatchupload-max-files-alert": "Você não pode enviar mais de $1 {{PLURAL:$1|arquivo|arquivos}} de uma só vez." 14 | } 15 | -------------------------------------------------------------------------------- /i18n/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Hamilton Abreu" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Permite o carregamento simples de ficheiros em segundo plano", 8 | "batchupload": "Carregar vários ficheiros", 9 | "simplebatchupload-buttonlabel": "Selecionar ficheiros (ou largue-os aqui)...", 10 | "simplebatchupload-comment": "Carregado com o [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Não pode carregar mais de $1 {{PLURAL:$1|ficheiro|ficheiros}} ao mesmo tempo." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/qqq.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "F.trott", 5 | "Kghbln" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "{{desc|name=SimpleBatchUpload|url=https://www.mediawiki.org/wiki/Extension:SimpleBatchUpload}}", 9 | "simplebatchupload-name": "{{Notranslate}} The name of the extesion as shown on special page 'Version'", 10 | "batchupload": "{{doc-special|BatchUpload}}", 11 | "simplebatchupload-buttonlabel": "The label for the upload button", 12 | "simplebatchupload-comment": "Comment saved with the upload. Do not translate '[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]'", 13 | "simplebatchupload-max-files-alert": "Alert saying there are too many files in a batch" 14 | } 15 | -------------------------------------------------------------------------------- /i18n/roa-tara.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Joetaras" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Permette le batch facile pe le carecaminde de le fail", 8 | "batchupload": "Careche cchiù file", 9 | "simplebatchupload-buttonlabel": "Scacchie le fail (o scittele aqquà)...", 10 | "simplebatchupload-comment": "Carecate cu [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Nojn ge puè carecà cchiù de $1 {{PLURAL:$1|fail}} jndr'à 'na vote." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Ivan-r" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Реализует простую пакетную загрузку файлов.", 8 | "batchupload": "Пакетная загрузка", 9 | "simplebatchupload-buttonlabel": "Выбрать файлы (или перетащите их сюда)…", 10 | "simplebatchupload-comment": "Загружено через [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Вы не можете загрузить более $1 {{PLURAL:$1|файла|файлов}} за раз." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Eleassar" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Omogoča preprosto paketno nalaganje datotek", 8 | "batchupload": "Nalaganje več datotek", 9 | "simplebatchupload-buttonlabel": "Izberite datoteke (ali jih izpustite tukaj) ...", 10 | "simplebatchupload-comment": "Naloženo s [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 11 | "simplebatchupload-max-files-alert": "Hkrati ne morete naložiti več kot $1 {{PLURAL:$1|datoteke|datotek}}." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/sr-ec.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Obsuser" 5 | ] 6 | }, 7 | "simplebatchupload-buttonlabel": "Одаберите датотеке (или их отпустите овде)...", 8 | "simplebatchupload-max-files-alert": "Не можете да отпремите више од $1 {{PLURAL:$1|датотеке|датотеке|датотека}} у исто време." 9 | } 10 | -------------------------------------------------------------------------------- /i18n/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "BaRaN6161 TURK" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "Dosyaların toplu olarak basitçe yüklenmesine izin verir", 8 | "batchupload": "Birden fazla dosya yükle", 9 | "simplebatchupload-buttonlabel": "Dosyaları seçin (veya buraya bırakın)...", 10 | "simplebatchupload-comment": "[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]] ile yüklendi", 11 | "simplebatchupload-max-files-alert": "Tek seferde $1 fazla dosya yükleyemezsiniz." 12 | } 13 | -------------------------------------------------------------------------------- /i18n/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Movses", 5 | "Piramidion" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "Надає доступ до простого пакетного завантаження файлів", 9 | "batchupload": "Завантаження декількох файлів", 10 | "simplebatchupload-buttonlabel": "Виберіть файли (або перетягніть їх сюди)...", 11 | "simplebatchupload-comment": "Завантажено за допомогою [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 12 | "simplebatchupload-max-files-alert": "Ви не можете завантажувати більше, ніж $1 {{PLURAL:$1|файл|файли|файлів}} водночас." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "NguyenHung", 5 | "SongNgu.xyz" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "Đơn giản hóa việc tải lên tập tin hàng loạt", 9 | "batchupload": "Tải lên nhiều tập tin", 10 | "simplebatchupload-buttonlabel": "Chọn tập tin (hoặc thả chúng vào đây)...", 11 | "simplebatchupload-comment": "Đã tải lên với [[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]", 12 | "simplebatchupload-max-files-alert": "Bạn không thể tải lên nhiều hơn $1 {{PLURAL:$1|tập tin}} cùng một lúc." 13 | } 14 | -------------------------------------------------------------------------------- /i18n/zh-hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Anterdc99", 5 | "LittlePaw365" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "允许简单地批量上传文件", 9 | "batchupload": "上传多个文件", 10 | "simplebatchupload-buttonlabel": "选择文件(或拖放文件到这里)...", 11 | "simplebatchupload-comment": "使用[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|SimpleBatchUpload]]上传", 12 | "simplebatchupload-max-files-alert": "您不能同时上传超过 $1 {{PLURAL:$1|个文件}}。" 13 | } 14 | -------------------------------------------------------------------------------- /i18n/zh-hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Kly", 5 | "Lakejason0" 6 | ] 7 | }, 8 | "simplebatchupload-desc": "允許簡易批次上傳檔案", 9 | "batchupload": "上傳多個檔案", 10 | "simplebatchupload-buttonlabel": "選擇檔案(或拖曳至此)…", 11 | "simplebatchupload-comment": "透過[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|簡易批次上傳]]來上傳", 12 | "simplebatchupload-max-files-alert": "您不能同時上傳超過 $1 {{PLURAL:$1|個檔案}}。" 13 | } 14 | -------------------------------------------------------------------------------- /i18n/zh-hk.json: -------------------------------------------------------------------------------- 1 | { 2 | "@metadata": { 3 | "authors": [ 4 | "Lakejason0" 5 | ] 6 | }, 7 | "simplebatchupload-desc": "容許簡易批次上載檔案", 8 | "batchupload": "上載多個檔案", 9 | "simplebatchupload-comment": "透過[[mw:Special:MyLanguage/Extension:SimpleBatchUpload|簡易批次上載]]來上載", 10 | "simplebatchupload-max-files-alert": "您不能同時上載超過 $1 {{PLURAL:$1|個檔案}}。" 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplebatchupload", 3 | "description": "Basic, no-frills uploading of multiple files to MediaWiki", 4 | "author": "Stephan Gambke", 5 | "license": "GPL-2.0-or-later", 6 | "scripts": { 7 | "pretest": "npm install", 8 | "test": "grunt test", 9 | "lint": "grunt lint", 10 | "preinstall": "rm -f res/jquery.fileupload.*", 11 | "install": "cp node_modules/blueimp-file-upload/js/jquery.fileupload.js node_modules/blueimp-file-upload/css/jquery.fileupload.css res" 12 | }, 13 | "devDependencies": { 14 | "grunt": "^0.4.0", 15 | "grunt-cli": "^0.1.13", 16 | "grunt-banana-checker": "^0.4.0", 17 | "grunt-contrib-jshint": "^1.0.0", 18 | "grunt-jsonlint": "^1.0.7", 19 | "blueimp-file-upload": "^v10.32.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | tests/phpunit 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | 3 | ### SimpleBatchUpload 2.0.1 4 | 5 | Released on December 7, 2023. 6 | 7 | * Added support for Composer 2.2 and above 8 | * Improved support for MediaWiki 1.41 and above 9 | 10 | ### SimpleBatchUpload 2.0.0 11 | 12 | Released on January 10, 2023. 13 | 14 | * Raised minimum required versions to 15 | * MediaWiki 1.35 16 | * PHP 8.0 17 | * Added PHP 8.1 support (thanks @malberts) 18 | * Fixed deprecation warning in MediaWiki 1.38 (thanks @malberts) 19 | 20 | ### SimpleBatchUpload 1.9.0 21 | 22 | Released on December 14, 2022. 23 | 24 | * Fixed jQuery Promise methods (thanks @malberts) 25 | * Updated `blueimp-file-upload` dependency to v10.32.0 (thanks @malberts) 26 | 27 | ### SimpleBatchUpload 1.8.2 28 | 29 | Released on May 5, 2021. 30 | 31 | * Fixed JavaScript loading issue on MediaWiki 1.35.x (thanks @MtMNC) 32 | 33 | ### SimpleBatchUpload 1.8.1 34 | 35 | Released on May 3, 2021. 36 | 37 | * Fix an issue in the previous patch causing batch uploading to break. 38 | 39 | ### SimpleBatchUpload 1.8.0 40 | 41 | Released on April 30, 2021. 42 | 43 | * Fix issues with multiple instances of `#batchupload` always inserting the content of the first instance. 44 | 45 | ### SimpleBatchUpload 1.7.0 46 | 47 | Released on April 13, 2021. 48 | 49 | * Added description field to the file upload form (by @thijskh) 50 | * Added `+rename` parameter to `#batchupload` to enable renaming of files via regex (by @ankostis) 51 | * Added file number to alert message (by @Abijeet) 52 | * Fixed compatibility issue with MediaWiki 1.35+ (by @thijskh) 53 | 54 | ### SimpleBatchUpload 1.6.0 55 | 56 | Released on March 24, 2020. 57 | 58 | * Added translations (via [translatewiki.net](https://translatewiki.net)) 59 | 60 | ### SimpleBatchUpload 1.5.0 61 | 62 | Released on November 10, 2019. 63 | 64 | Changes: 65 | * Raise minimum required versions to 66 | * MediaWiki 1.31 67 | * PHP 7.0 68 | * Add CI testing 69 | * Ensure compatibility with MediaWiki 1.32+ ([#21](https://github.com/ProfessionalWiki/SimpleBatchUpload/issues/21)) 70 | 71 | ### SimpleBatchUpload 1.4.0 72 | 73 | Released on October 24, 2018. 74 | 75 | Changes: 76 | * New configuration parameter `$wgSimpleBatchUploadMaxFilesPerBatch` 77 | 78 | ### SimpleBatchUpload 1.3.2 79 | 80 | Released on October 12, 2018. 81 | 82 | Changes: 83 | * Fix for unauthenticated arbitrary file upload vulnerability in Blueimp 84 | jQuery-File-Upload <= v9.22.0 ([CVE-2018-9206](https://nvd.nist.gov/vuln/detail/CVE-2018-9206)) 85 | (this also fixes the issue where the extension does not work in debug=true mode) 86 | 87 | ### SimpleBatchUpload 1.3.1 88 | 89 | Released on April 18, 2018. 90 | 91 | Changes: 92 | * Fix tarball installation 93 | 94 | ### SimpleBatchUpload 1.3.0 95 | 96 | Released on March 30, 2018. 97 | 98 | Changes: 99 | * Add parser function `#batchupload` 100 | * Improve error messages 101 | 102 | ### SimpleBatchUpload 1.2.0 103 | 104 | Released on February 9, 2017. 105 | 106 | Changes: 107 | * Add a summary/comment for each upload 108 | * Read upload parameters from system message "MediaWiki:Simplebatchupload-parameters" 109 | * Improved build script 110 | * Fix failed uploads due timed-out edit token 111 | * Fix "extension.json" for MW 1.26: Remove `load_composer_autoloader` 112 | * Enable linting of JS, JSON and i18n files: 113 | Run `npm install && npm run lint` from the extension directory 114 | * Improve code quality 115 | 116 | ### SimpleBatchUpload 1.1.0 117 | 118 | Released on June 10, 2016. 119 | 120 | Changes: 121 | * Add progress indicators 122 | * Delete result list before initiating new upload 123 | 124 | ### SimpleBatchUpload 1.0.1 125 | 126 | Released on June 6, 2016. 127 | 128 | Changes: 129 | * Add documentation 130 | * Fix error handling 131 | * Fix minimum MW version to 1.26 132 | * Fix i18n of upload button label 133 | 134 | ### SimpleBatchUpload 1.0.0 135 | 136 | Released on June 6, 2016. 137 | 138 | First version 139 | -------------------------------------------------------------------------------- /res/ext.SimpleBatchUpload.css: -------------------------------------------------------------------------------- 1 | span.fileinput-button { 2 | -moz-user-select: none; 3 | background-color: #f9f9f9; 4 | background-image: none; 5 | border: 1px solid #cccccc; 6 | border-radius: 4px; 7 | color: #333333; 8 | cursor: pointer; 9 | display: inline-block; 10 | font-size: 14px; 11 | font-weight: normal; 12 | line-height: 1.42857; 13 | margin-bottom: 0; 14 | padding: 6px 12px; 15 | text-align: center; 16 | vertical-align: middle; 17 | white-space: nowrap; 18 | } 19 | 20 | ul.fileupload-results { 21 | list-style: outside none none; 22 | margin-top: 1em; 23 | padding-left: 0; 24 | } 25 | 26 | ul.fileupload-results li { 27 | padding: 0.2em 1em; 28 | } 29 | 30 | ul.fileupload-results li.ful-success{ 31 | background-color: lightgreen; 32 | } 33 | 34 | ul.fileupload-results li.ful-success a { 35 | color: #0645ad; 36 | } 37 | 38 | ul.fileupload-results li.ful-error{ 39 | background-color: lightpink; 40 | } 41 | -------------------------------------------------------------------------------- /res/ext.SimpleBatchUpload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File containing the SimpleBatchUpload class 3 | * 4 | * @copyright (C) 2016 - 2017, Stephan Gambke 5 | * @license GNU General Public License, version 2 (or any later version) 6 | * 7 | * This software is free software; you can redistribute it and/or 8 | * modify it under the terms of the GNU General Public License 9 | * as published by the Free Software Foundation; either version 2 10 | * of the License, or (at your option) any later version. 11 | * This software is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, see . 17 | * 18 | * @file 19 | * @ingroup SimpleBatchUpload 20 | */ 21 | 22 | /** global: mediaWiki */ 23 | /** global: jQuery */ 24 | 25 | ;( function ( $, mw, undefined ) { 26 | 27 | 'use strict'; 28 | 29 | $( function () { 30 | 31 | var filesLimitPerBatchConfig = mw.config.get( 'simpleBatchUploadMaxFilesPerBatch' ), 32 | userGroups = mw.config.get( 'wgUserGroups' ), 33 | userUploadLimit = 0; 34 | 35 | if ( filesLimitPerBatchConfig ) { 36 | $.each( filesLimitPerBatchConfig, function ( role, limit ) { 37 | if ( userGroups.indexOf( role ) !== -1 && ( limit > userUploadLimit ) ) { 38 | userUploadLimit = limit; 39 | } 40 | } ); 41 | } 42 | 43 | $( 'div.fileupload-container' ).each( function () { 44 | 45 | var container = this; 46 | 47 | $( 'input.fileupload', container ) 48 | 49 | .on( 'change drop', function ( /* e, data */ ) { $( 'ul.fileupload-results', container ).empty(); } ) 50 | 51 | .fileupload( { 52 | dataType: 'json', 53 | dropZone: $( '.fileupload-dropzone', container ), 54 | progressInterval: 100, 55 | 56 | add: function ( e, data ) { 57 | 58 | var that = this; 59 | 60 | if ( data.originalFiles.length > userUploadLimit ) { 61 | window.alert( mw.msg( 'simplebatchupload-max-files-alert', userUploadLimit ) ); 62 | return false; 63 | } 64 | 65 | data.id = Date.now(); 66 | 67 | var src_filename = data.files[ 0 ].name; 68 | var filenode_text = src_filename; 69 | var dst_filename = src_filename 70 | var textdata = $(container).find('[name="wfUploadDescription"]').val(); 71 | // It matches: 72 | // other| +rename = !(\w+)[ -_/]*! =$1-}} 73 | // where: 74 | // what: (\w+)[ -_/]* 75 | // with: $1- 76 | // Spaces are important in subst-pattern (after 2nd '='). 77 | var rename_regex = /\|\s*\+rename\s*=\s*([#\/@!])(.+)\1([gimuy]{0,5})\s*-->(.*?)(?=\||}}\s*$)/; 78 | var match = rename_regex.exec(textdata); 79 | if ( match ) { 80 | var pattern = RegExp(match[2], match[3]); 81 | var replace = match[4]; 82 | dst_filename = src_filename.replace(pattern, replace); 83 | filenode_text = ( dst_filename == src_filename ) ? 84 | src_filename : `${src_filename} --> ${dst_filename}`; 85 | } 86 | 87 | var status = $( '
  • ' ) 88 | .attr( 'id', data.id ) 89 | .text( filenode_text ) 90 | .data('filenode_text', filenode_text); 91 | 92 | $( 'ul.fileupload-results', container ).append( status ); 93 | 94 | var api = new mw.Api(); 95 | 96 | var tokenType = 'csrf'; 97 | 98 | // invalidate cached token; always request a new one 99 | api.badToken( tokenType ); 100 | 101 | api.getToken( tokenType ) 102 | .then( 103 | function ( token ) { 104 | 105 | data.formData = { 106 | format: 'json', 107 | action: 'upload', 108 | token: token, 109 | ignorewarnings: 1, 110 | text: textdata.replace(rename_regex, ''), 111 | comment: $( that ).fileupload( 'option', 'comment' ), 112 | filename: dst_filename 113 | }; 114 | 115 | data.submit() 116 | .done( function ( result /*, textStatus, jqXHR */ ) { 117 | 118 | if ( result.error !== undefined ) { 119 | 120 | status.text( status.text() + " ERROR: " + result.error.info ).addClass( 'ful-error api-error' ); 121 | 122 | } else { 123 | var link = $( '' ); 124 | link 125 | .attr( 'href', mw.Title.newFromFileName( result.upload.filename ).getUrl() ) 126 | .text( status.data('filenode_text') ); 127 | 128 | status 129 | .addClass( 'ful-success' ) 130 | .text( ' OK' ) 131 | .prepend( link ); 132 | } 133 | 134 | } ) 135 | .fail( function ( /* jqXHR, textStatus, errorThrown */ ) { 136 | status.text( status.text() + " ERROR: Server communication failed." ).addClass( 'ful-error server-error' ); 137 | // console.log( JSON.stringify( arguments ) ); 138 | } ); 139 | }, 140 | function () { 141 | status.text( status.text() + " ERROR: Could not get token." ).addClass( 'ful-error token-error' ); 142 | // console.log( JSON.stringify( arguments ) ); 143 | } 144 | ); 145 | 146 | }, 147 | 148 | progress: function ( e, data ) { 149 | if ( data.loaded !== data.total ) { 150 | var status = $( '#' + data.id ); 151 | status.text( status.data('filenode_text') + ' ' + parseInt( data.loaded / data.total * 100, 10 ) + '%' ); 152 | } 153 | } 154 | } ); 155 | } ); 156 | 157 | $( document ).bind( 'drop dragover', function ( e ) { 158 | e.preventDefault(); 159 | } ); 160 | } ); 161 | 162 | }( jQuery, mediaWiki )); 163 | -------------------------------------------------------------------------------- /res/jquery.fileupload.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin CSS 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button { 14 | position: relative; 15 | overflow: hidden; 16 | display: inline-block; 17 | } 18 | .fileinput-button input { 19 | position: absolute; 20 | top: 0; 21 | right: 0; 22 | margin: 0; 23 | height: 100%; 24 | opacity: 0; 25 | filter: alpha(opacity=0); 26 | font-size: 200px !important; 27 | direction: ltr; 28 | cursor: pointer; 29 | } 30 | 31 | /* Fixes for IE < 8 */ 32 | @media screen\9 { 33 | .fileinput-button input { 34 | font-size: 150% !important; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /res/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | /* eslint-disable new-cap */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define(['jquery', 'jquery-ui/ui/widget'], factory); 20 | } else if (typeof exports === 'object') { 21 | // Node/CommonJS: 22 | factory(require('jquery'), require('./vendor/jquery.ui.widget')); 23 | } else { 24 | // Browser globals: 25 | factory(window.jQuery); 26 | } 27 | })(function ($) { 28 | 'use strict'; 29 | 30 | // Detect file input support, based on 31 | // https://viljamis.com/2012/file-upload-support-on-mobile/ 32 | $.support.fileInput = !( 33 | new RegExp( 34 | // Handle devices which give false positives for the feature detection: 35 | '(Android (1\\.[0156]|2\\.[01]))' + 36 | '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + 37 | '|(w(eb)?OSBrowser)|(webOS)' + 38 | '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' 39 | ).test(window.navigator.userAgent) || 40 | // Feature detection for all other devices: 41 | $('').prop('disabled') 42 | ); 43 | 44 | // The FileReader API is not actually used, but works as feature detection, 45 | // as some Safari versions (5?) support XHR file uploads via the FormData API, 46 | // but not non-multipart XHR file uploads. 47 | // window.XMLHttpRequestUpload is not available on IE10, so we check for 48 | // window.ProgressEvent instead to detect XHR2 file upload capability: 49 | $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); 50 | $.support.xhrFormDataFileUpload = !!window.FormData; 51 | 52 | // Detect support for Blob slicing (required for chunked uploads): 53 | $.support.blobSlice = 54 | window.Blob && 55 | (Blob.prototype.slice || 56 | Blob.prototype.webkitSlice || 57 | Blob.prototype.mozSlice); 58 | 59 | /** 60 | * Helper function to create drag handlers for dragover/dragenter/dragleave 61 | * 62 | * @param {string} type Event type 63 | * @returns {Function} Drag handler 64 | */ 65 | function getDragHandler(type) { 66 | var isDragOver = type === 'dragover'; 67 | return function (e) { 68 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 69 | var dataTransfer = e.dataTransfer; 70 | if ( 71 | dataTransfer && 72 | $.inArray('Files', dataTransfer.types) !== -1 && 73 | this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false 74 | ) { 75 | e.preventDefault(); 76 | if (isDragOver) { 77 | dataTransfer.dropEffect = 'copy'; 78 | } 79 | } 80 | }; 81 | } 82 | 83 | // The fileupload widget listens for change events on file input fields defined 84 | // via fileInput setting and paste or drop events of the given dropZone. 85 | // In addition to the default jQuery Widget methods, the fileupload widget 86 | // exposes the "add" and "send" methods, to add or directly send files using 87 | // the fileupload API. 88 | // By default, files added via file input selection, paste, drag & drop or 89 | // "add" method are uploaded immediately, but it is possible to override 90 | // the "add" callback option to queue file uploads. 91 | $.widget('blueimp.fileupload', { 92 | options: { 93 | // The drop target element(s), by the default the complete document. 94 | // Set to null to disable drag & drop support: 95 | dropZone: $(document), 96 | // The paste target element(s), by the default undefined. 97 | // Set to a DOM node or jQuery object to enable file pasting: 98 | pasteZone: undefined, 99 | // The file input field(s), that are listened to for change events. 100 | // If undefined, it is set to the file input fields inside 101 | // of the widget element on plugin initialization. 102 | // Set to null to disable the change listener. 103 | fileInput: undefined, 104 | // By default, the file input field is replaced with a clone after 105 | // each input field change event. This is required for iframe transport 106 | // queues and allows change events to be fired for the same file 107 | // selection, but can be disabled by setting the following option to false: 108 | replaceFileInput: true, 109 | // The parameter name for the file form data (the request argument name). 110 | // If undefined or empty, the name property of the file input field is 111 | // used, or "files[]" if the file input name property is also empty, 112 | // can be a string or an array of strings: 113 | paramName: undefined, 114 | // By default, each file of a selection is uploaded using an individual 115 | // request for XHR type uploads. Set to false to upload file 116 | // selections in one request each: 117 | singleFileUploads: true, 118 | // To limit the number of files uploaded with one XHR request, 119 | // set the following option to an integer greater than 0: 120 | limitMultiFileUploads: undefined, 121 | // The following option limits the number of files uploaded with one 122 | // XHR request to keep the request size under or equal to the defined 123 | // limit in bytes: 124 | limitMultiFileUploadSize: undefined, 125 | // Multipart file uploads add a number of bytes to each uploaded file, 126 | // therefore the following option adds an overhead for each file used 127 | // in the limitMultiFileUploadSize configuration: 128 | limitMultiFileUploadSizeOverhead: 512, 129 | // Set the following option to true to issue all file upload requests 130 | // in a sequential order: 131 | sequentialUploads: false, 132 | // To limit the number of concurrent uploads, 133 | // set the following option to an integer greater than 0: 134 | limitConcurrentUploads: undefined, 135 | // Set the following option to true to force iframe transport uploads: 136 | forceIframeTransport: false, 137 | // Set the following option to the location of a redirect url on the 138 | // origin server, for cross-domain iframe transport uploads: 139 | redirect: undefined, 140 | // The parameter name for the redirect url, sent as part of the form 141 | // data and set to 'redirect' if this option is empty: 142 | redirectParamName: undefined, 143 | // Set the following option to the location of a postMessage window, 144 | // to enable postMessage transport uploads: 145 | postMessage: undefined, 146 | // By default, XHR file uploads are sent as multipart/form-data. 147 | // The iframe transport is always using multipart/form-data. 148 | // Set to false to enable non-multipart XHR uploads: 149 | multipart: true, 150 | // To upload large files in smaller chunks, set the following option 151 | // to a preferred maximum chunk size. If set to 0, null or undefined, 152 | // or the browser does not support the required Blob API, files will 153 | // be uploaded as a whole. 154 | maxChunkSize: undefined, 155 | // When a non-multipart upload or a chunked multipart upload has been 156 | // aborted, this option can be used to resume the upload by setting 157 | // it to the size of the already uploaded bytes. This option is most 158 | // useful when modifying the options object inside of the "add" or 159 | // "send" callbacks, as the options are cloned for each file upload. 160 | uploadedBytes: undefined, 161 | // By default, failed (abort or error) file uploads are removed from the 162 | // global progress calculation. Set the following option to false to 163 | // prevent recalculating the global progress data: 164 | recalculateProgress: true, 165 | // Interval in milliseconds to calculate and trigger progress events: 166 | progressInterval: 100, 167 | // Interval in milliseconds to calculate progress bitrate: 168 | bitrateInterval: 500, 169 | // By default, uploads are started automatically when adding files: 170 | autoUpload: true, 171 | // By default, duplicate file names are expected to be handled on 172 | // the server-side. If this is not possible (e.g. when uploading 173 | // files directly to Amazon S3), the following option can be set to 174 | // an empty object or an object mapping existing filenames, e.g.: 175 | // { "image.jpg": true, "image (1).jpg": true } 176 | // If it is set, all files will be uploaded with unique filenames, 177 | // adding increasing number suffixes if necessary, e.g.: 178 | // "image (2).jpg" 179 | uniqueFilenames: undefined, 180 | 181 | // Error and info messages: 182 | messages: { 183 | uploadedBytes: 'Uploaded bytes exceed file size' 184 | }, 185 | 186 | // Translation function, gets the message key to be translated 187 | // and an object with context specific data as arguments: 188 | i18n: function (message, context) { 189 | // eslint-disable-next-line no-param-reassign 190 | message = this.messages[message] || message.toString(); 191 | if (context) { 192 | $.each(context, function (key, value) { 193 | // eslint-disable-next-line no-param-reassign 194 | message = message.replace('{' + key + '}', value); 195 | }); 196 | } 197 | return message; 198 | }, 199 | 200 | // Additional form data to be sent along with the file uploads can be set 201 | // using this option, which accepts an array of objects with name and 202 | // value properties, a function returning such an array, a FormData 203 | // object (for XHR file uploads), or a simple object. 204 | // The form of the first fileInput is given as parameter to the function: 205 | formData: function (form) { 206 | return form.serializeArray(); 207 | }, 208 | 209 | // The add callback is invoked as soon as files are added to the fileupload 210 | // widget (via file input selection, drag & drop, paste or add API call). 211 | // If the singleFileUploads option is enabled, this callback will be 212 | // called once for each file in the selection for XHR file uploads, else 213 | // once for each file selection. 214 | // 215 | // The upload starts when the submit method is invoked on the data parameter. 216 | // The data object contains a files property holding the added files 217 | // and allows you to override plugin options as well as define ajax settings. 218 | // 219 | // Listeners for this callback can also be bound the following way: 220 | // .on('fileuploadadd', func); 221 | // 222 | // data.submit() returns a Promise object and allows to attach additional 223 | // handlers using jQuery's Deferred callbacks: 224 | // data.submit().done(func).fail(func).always(func); 225 | add: function (e, data) { 226 | if (e.isDefaultPrevented()) { 227 | return false; 228 | } 229 | if ( 230 | data.autoUpload || 231 | (data.autoUpload !== false && 232 | $(this).fileupload('option', 'autoUpload')) 233 | ) { 234 | data.process().done(function () { 235 | data.submit(); 236 | }); 237 | } 238 | }, 239 | 240 | // Other callbacks: 241 | 242 | // Callback for the submit event of each file upload: 243 | // submit: function (e, data) {}, // .on('fileuploadsubmit', func); 244 | 245 | // Callback for the start of each file upload request: 246 | // send: function (e, data) {}, // .on('fileuploadsend', func); 247 | 248 | // Callback for successful uploads: 249 | // done: function (e, data) {}, // .on('fileuploaddone', func); 250 | 251 | // Callback for failed (abort or error) uploads: 252 | // fail: function (e, data) {}, // .on('fileuploadfail', func); 253 | 254 | // Callback for completed (success, abort or error) requests: 255 | // always: function (e, data) {}, // .on('fileuploadalways', func); 256 | 257 | // Callback for upload progress events: 258 | // progress: function (e, data) {}, // .on('fileuploadprogress', func); 259 | 260 | // Callback for global upload progress events: 261 | // progressall: function (e, data) {}, // .on('fileuploadprogressall', func); 262 | 263 | // Callback for uploads start, equivalent to the global ajaxStart event: 264 | // start: function (e) {}, // .on('fileuploadstart', func); 265 | 266 | // Callback for uploads stop, equivalent to the global ajaxStop event: 267 | // stop: function (e) {}, // .on('fileuploadstop', func); 268 | 269 | // Callback for change events of the fileInput(s): 270 | // change: function (e, data) {}, // .on('fileuploadchange', func); 271 | 272 | // Callback for paste events to the pasteZone(s): 273 | // paste: function (e, data) {}, // .on('fileuploadpaste', func); 274 | 275 | // Callback for drop events of the dropZone(s): 276 | // drop: function (e, data) {}, // .on('fileuploaddrop', func); 277 | 278 | // Callback for dragover events of the dropZone(s): 279 | // dragover: function (e) {}, // .on('fileuploaddragover', func); 280 | 281 | // Callback before the start of each chunk upload request (before form data initialization): 282 | // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func); 283 | 284 | // Callback for the start of each chunk upload request: 285 | // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func); 286 | 287 | // Callback for successful chunk uploads: 288 | // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func); 289 | 290 | // Callback for failed (abort or error) chunk uploads: 291 | // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func); 292 | 293 | // Callback for completed (success, abort or error) chunk upload requests: 294 | // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func); 295 | 296 | // The plugin options are used as settings object for the ajax calls. 297 | // The following are jQuery ajax settings required for the file uploads: 298 | processData: false, 299 | contentType: false, 300 | cache: false, 301 | timeout: 0 302 | }, 303 | 304 | // jQuery versions before 1.8 require promise.pipe if the return value is 305 | // used, as promise.then in older versions has a different behavior, see: 306 | // https://blog.jquery.com/2012/08/09/jquery-1-8-released/ 307 | // https://bugs.jquery.com/ticket/11010 308 | // https://github.com/blueimp/jQuery-File-Upload/pull/3435 309 | _promisePipe: (function () { 310 | var parts = $.fn.jquery.split('.'); 311 | return Number(parts[0]) > 1 || Number(parts[1]) > 7 ? 'then' : 'pipe'; 312 | })(), 313 | 314 | // A list of options that require reinitializing event listeners and/or 315 | // special initialization code: 316 | _specialOptions: [ 317 | 'fileInput', 318 | 'dropZone', 319 | 'pasteZone', 320 | 'multipart', 321 | 'forceIframeTransport' 322 | ], 323 | 324 | _blobSlice: 325 | $.support.blobSlice && 326 | function () { 327 | var slice = this.slice || this.webkitSlice || this.mozSlice; 328 | return slice.apply(this, arguments); 329 | }, 330 | 331 | _BitrateTimer: function () { 332 | this.timestamp = Date.now ? Date.now() : new Date().getTime(); 333 | this.loaded = 0; 334 | this.bitrate = 0; 335 | this.getBitrate = function (now, loaded, interval) { 336 | var timeDiff = now - this.timestamp; 337 | if (!this.bitrate || !interval || timeDiff > interval) { 338 | this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; 339 | this.loaded = loaded; 340 | this.timestamp = now; 341 | } 342 | return this.bitrate; 343 | }; 344 | }, 345 | 346 | _isXHRUpload: function (options) { 347 | return ( 348 | !options.forceIframeTransport && 349 | ((!options.multipart && $.support.xhrFileUpload) || 350 | $.support.xhrFormDataFileUpload) 351 | ); 352 | }, 353 | 354 | _getFormData: function (options) { 355 | var formData; 356 | if ($.type(options.formData) === 'function') { 357 | return options.formData(options.form); 358 | } 359 | if ($.isArray(options.formData)) { 360 | return options.formData; 361 | } 362 | if ($.type(options.formData) === 'object') { 363 | formData = []; 364 | $.each(options.formData, function (name, value) { 365 | formData.push({ name: name, value: value }); 366 | }); 367 | return formData; 368 | } 369 | return []; 370 | }, 371 | 372 | _getTotal: function (files) { 373 | var total = 0; 374 | $.each(files, function (index, file) { 375 | total += file.size || 1; 376 | }); 377 | return total; 378 | }, 379 | 380 | _initProgressObject: function (obj) { 381 | var progress = { 382 | loaded: 0, 383 | total: 0, 384 | bitrate: 0 385 | }; 386 | if (obj._progress) { 387 | $.extend(obj._progress, progress); 388 | } else { 389 | obj._progress = progress; 390 | } 391 | }, 392 | 393 | _initResponseObject: function (obj) { 394 | var prop; 395 | if (obj._response) { 396 | for (prop in obj._response) { 397 | if (Object.prototype.hasOwnProperty.call(obj._response, prop)) { 398 | delete obj._response[prop]; 399 | } 400 | } 401 | } else { 402 | obj._response = {}; 403 | } 404 | }, 405 | 406 | _onProgress: function (e, data) { 407 | if (e.lengthComputable) { 408 | var now = Date.now ? Date.now() : new Date().getTime(), 409 | loaded; 410 | if ( 411 | data._time && 412 | data.progressInterval && 413 | now - data._time < data.progressInterval && 414 | e.loaded !== e.total 415 | ) { 416 | return; 417 | } 418 | data._time = now; 419 | loaded = 420 | Math.floor( 421 | (e.loaded / e.total) * (data.chunkSize || data._progress.total) 422 | ) + (data.uploadedBytes || 0); 423 | // Add the difference from the previously loaded state 424 | // to the global loaded counter: 425 | this._progress.loaded += loaded - data._progress.loaded; 426 | this._progress.bitrate = this._bitrateTimer.getBitrate( 427 | now, 428 | this._progress.loaded, 429 | data.bitrateInterval 430 | ); 431 | data._progress.loaded = data.loaded = loaded; 432 | data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( 433 | now, 434 | loaded, 435 | data.bitrateInterval 436 | ); 437 | // Trigger a custom progress event with a total data property set 438 | // to the file size(s) of the current upload and a loaded data 439 | // property calculated accordingly: 440 | this._trigger( 441 | 'progress', 442 | $.Event('progress', { delegatedEvent: e }), 443 | data 444 | ); 445 | // Trigger a global progress event for all current file uploads, 446 | // including ajax calls queued for sequential file uploads: 447 | this._trigger( 448 | 'progressall', 449 | $.Event('progressall', { delegatedEvent: e }), 450 | this._progress 451 | ); 452 | } 453 | }, 454 | 455 | _initProgressListener: function (options) { 456 | var that = this, 457 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 458 | // Access to the native XHR object is required to add event listeners 459 | // for the upload progress event: 460 | if (xhr.upload) { 461 | $(xhr.upload).on('progress', function (e) { 462 | var oe = e.originalEvent; 463 | // Make sure the progress event properties get copied over: 464 | e.lengthComputable = oe.lengthComputable; 465 | e.loaded = oe.loaded; 466 | e.total = oe.total; 467 | that._onProgress(e, options); 468 | }); 469 | options.xhr = function () { 470 | return xhr; 471 | }; 472 | } 473 | }, 474 | 475 | _deinitProgressListener: function (options) { 476 | var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 477 | if (xhr.upload) { 478 | $(xhr.upload).off('progress'); 479 | } 480 | }, 481 | 482 | _isInstanceOf: function (type, obj) { 483 | // Cross-frame instanceof check 484 | return Object.prototype.toString.call(obj) === '[object ' + type + ']'; 485 | }, 486 | 487 | _getUniqueFilename: function (name, map) { 488 | // eslint-disable-next-line no-param-reassign 489 | name = String(name); 490 | if (map[name]) { 491 | // eslint-disable-next-line no-param-reassign 492 | name = name.replace( 493 | /(?: \(([\d]+)\))?(\.[^.]+)?$/, 494 | function (_, p1, p2) { 495 | var index = p1 ? Number(p1) + 1 : 1; 496 | var ext = p2 || ''; 497 | return ' (' + index + ')' + ext; 498 | } 499 | ); 500 | return this._getUniqueFilename(name, map); 501 | } 502 | map[name] = true; 503 | return name; 504 | }, 505 | 506 | _initXHRData: function (options) { 507 | var that = this, 508 | formData, 509 | file = options.files[0], 510 | // Ignore non-multipart setting if not supported: 511 | multipart = options.multipart || !$.support.xhrFileUpload, 512 | paramName = 513 | $.type(options.paramName) === 'array' 514 | ? options.paramName[0] 515 | : options.paramName; 516 | options.headers = $.extend({}, options.headers); 517 | if (options.contentRange) { 518 | options.headers['Content-Range'] = options.contentRange; 519 | } 520 | if (!multipart || options.blob || !this._isInstanceOf('File', file)) { 521 | options.headers['Content-Disposition'] = 522 | 'attachment; filename="' + 523 | encodeURI(file.uploadName || file.name) + 524 | '"'; 525 | } 526 | if (!multipart) { 527 | options.contentType = file.type || 'application/octet-stream'; 528 | options.data = options.blob || file; 529 | } else if ($.support.xhrFormDataFileUpload) { 530 | if (options.postMessage) { 531 | // window.postMessage does not allow sending FormData 532 | // objects, so we just add the File/Blob objects to 533 | // the formData array and let the postMessage window 534 | // create the FormData object out of this array: 535 | formData = this._getFormData(options); 536 | if (options.blob) { 537 | formData.push({ 538 | name: paramName, 539 | value: options.blob 540 | }); 541 | } else { 542 | $.each(options.files, function (index, file) { 543 | formData.push({ 544 | name: 545 | ($.type(options.paramName) === 'array' && 546 | options.paramName[index]) || 547 | paramName, 548 | value: file 549 | }); 550 | }); 551 | } 552 | } else { 553 | if (that._isInstanceOf('FormData', options.formData)) { 554 | formData = options.formData; 555 | } else { 556 | formData = new FormData(); 557 | $.each(this._getFormData(options), function (index, field) { 558 | formData.append(field.name, field.value); 559 | }); 560 | } 561 | if (options.blob) { 562 | formData.append( 563 | paramName, 564 | options.blob, 565 | file.uploadName || file.name 566 | ); 567 | } else { 568 | $.each(options.files, function (index, file) { 569 | // This check allows the tests to run with 570 | // dummy objects: 571 | if ( 572 | that._isInstanceOf('File', file) || 573 | that._isInstanceOf('Blob', file) 574 | ) { 575 | var fileName = file.uploadName || file.name; 576 | if (options.uniqueFilenames) { 577 | fileName = that._getUniqueFilename( 578 | fileName, 579 | options.uniqueFilenames 580 | ); 581 | } 582 | formData.append( 583 | ($.type(options.paramName) === 'array' && 584 | options.paramName[index]) || 585 | paramName, 586 | file, 587 | fileName 588 | ); 589 | } 590 | }); 591 | } 592 | } 593 | options.data = formData; 594 | } 595 | // Blob reference is not needed anymore, free memory: 596 | options.blob = null; 597 | }, 598 | 599 | _initIframeSettings: function (options) { 600 | var targetHost = $('').prop('href', options.url).prop('host'); 601 | // Setting the dataType to iframe enables the iframe transport: 602 | options.dataType = 'iframe ' + (options.dataType || ''); 603 | // The iframe transport accepts a serialized array as form data: 604 | options.formData = this._getFormData(options); 605 | // Add redirect url to form data on cross-domain uploads: 606 | if (options.redirect && targetHost && targetHost !== location.host) { 607 | options.formData.push({ 608 | name: options.redirectParamName || 'redirect', 609 | value: options.redirect 610 | }); 611 | } 612 | }, 613 | 614 | _initDataSettings: function (options) { 615 | if (this._isXHRUpload(options)) { 616 | if (!this._chunkedUpload(options, true)) { 617 | if (!options.data) { 618 | this._initXHRData(options); 619 | } 620 | this._initProgressListener(options); 621 | } 622 | if (options.postMessage) { 623 | // Setting the dataType to postmessage enables the 624 | // postMessage transport: 625 | options.dataType = 'postmessage ' + (options.dataType || ''); 626 | } 627 | } else { 628 | this._initIframeSettings(options); 629 | } 630 | }, 631 | 632 | _getParamName: function (options) { 633 | var fileInput = $(options.fileInput), 634 | paramName = options.paramName; 635 | if (!paramName) { 636 | paramName = []; 637 | fileInput.each(function () { 638 | var input = $(this), 639 | name = input.prop('name') || 'files[]', 640 | i = (input.prop('files') || [1]).length; 641 | while (i) { 642 | paramName.push(name); 643 | i -= 1; 644 | } 645 | }); 646 | if (!paramName.length) { 647 | paramName = [fileInput.prop('name') || 'files[]']; 648 | } 649 | } else if (!$.isArray(paramName)) { 650 | paramName = [paramName]; 651 | } 652 | return paramName; 653 | }, 654 | 655 | _initFormSettings: function (options) { 656 | // Retrieve missing options from the input field and the 657 | // associated form, if available: 658 | if (!options.form || !options.form.length) { 659 | options.form = $(options.fileInput.prop('form')); 660 | // If the given file input doesn't have an associated form, 661 | // use the default widget file input's form: 662 | if (!options.form.length) { 663 | options.form = $(this.options.fileInput.prop('form')); 664 | } 665 | } 666 | options.paramName = this._getParamName(options); 667 | if (!options.url) { 668 | options.url = options.form.prop('action') || location.href; 669 | } 670 | // The HTTP request method must be "POST" or "PUT": 671 | options.type = ( 672 | options.type || 673 | ($.type(options.form.prop('method')) === 'string' && 674 | options.form.prop('method')) || 675 | '' 676 | ).toUpperCase(); 677 | if ( 678 | options.type !== 'POST' && 679 | options.type !== 'PUT' && 680 | options.type !== 'PATCH' 681 | ) { 682 | options.type = 'POST'; 683 | } 684 | if (!options.formAcceptCharset) { 685 | options.formAcceptCharset = options.form.attr('accept-charset'); 686 | } 687 | }, 688 | 689 | _getAJAXSettings: function (data) { 690 | var options = $.extend({}, this.options, data); 691 | this._initFormSettings(options); 692 | this._initDataSettings(options); 693 | return options; 694 | }, 695 | 696 | // jQuery 1.6 doesn't provide .state(), 697 | // while jQuery 1.8+ removed .isRejected() and .isResolved(): 698 | _getDeferredState: function (deferred) { 699 | if (deferred.state) { 700 | return deferred.state(); 701 | } 702 | if (deferred.isResolved()) { 703 | return 'resolved'; 704 | } 705 | if (deferred.isRejected()) { 706 | return 'rejected'; 707 | } 708 | return 'pending'; 709 | }, 710 | 711 | // Maps jqXHR callbacks to the equivalent 712 | // methods of the given Promise object: 713 | _enhancePromise: function (promise) { 714 | promise.success = promise.done; 715 | promise.error = promise.fail; 716 | promise.complete = promise.always; 717 | return promise; 718 | }, 719 | 720 | // Creates and returns a Promise object enhanced with 721 | // the jqXHR methods abort, success, error and complete: 722 | _getXHRPromise: function (resolveOrReject, context, args) { 723 | var dfd = $.Deferred(), 724 | promise = dfd.promise(); 725 | // eslint-disable-next-line no-param-reassign 726 | context = context || this.options.context || promise; 727 | if (resolveOrReject === true) { 728 | dfd.resolveWith(context, args); 729 | } else if (resolveOrReject === false) { 730 | dfd.rejectWith(context, args); 731 | } 732 | promise.abort = dfd.promise; 733 | return this._enhancePromise(promise); 734 | }, 735 | 736 | // Adds convenience methods to the data callback argument: 737 | _addConvenienceMethods: function (e, data) { 738 | var that = this, 739 | getPromise = function (args) { 740 | return $.Deferred().resolveWith(that, args).promise(); 741 | }; 742 | data.process = function (resolveFunc, rejectFunc) { 743 | if (resolveFunc || rejectFunc) { 744 | data._processQueue = this._processQueue = (this._processQueue || 745 | getPromise([this])) 746 | [that._promisePipe](function () { 747 | if (data.errorThrown) { 748 | return $.Deferred().rejectWith(that, [data]).promise(); 749 | } 750 | return getPromise(arguments); 751 | }) 752 | [that._promisePipe](resolveFunc, rejectFunc); 753 | } 754 | return this._processQueue || getPromise([this]); 755 | }; 756 | data.submit = function () { 757 | if (this.state() !== 'pending') { 758 | data.jqXHR = this.jqXHR = 759 | that._trigger( 760 | 'submit', 761 | $.Event('submit', { delegatedEvent: e }), 762 | this 763 | ) !== false && that._onSend(e, this); 764 | } 765 | return this.jqXHR || that._getXHRPromise(); 766 | }; 767 | data.abort = function () { 768 | if (this.jqXHR) { 769 | return this.jqXHR.abort(); 770 | } 771 | this.errorThrown = 'abort'; 772 | that._trigger('fail', null, this); 773 | return that._getXHRPromise(false); 774 | }; 775 | data.state = function () { 776 | if (this.jqXHR) { 777 | return that._getDeferredState(this.jqXHR); 778 | } 779 | if (this._processQueue) { 780 | return that._getDeferredState(this._processQueue); 781 | } 782 | }; 783 | data.processing = function () { 784 | return ( 785 | !this.jqXHR && 786 | this._processQueue && 787 | that._getDeferredState(this._processQueue) === 'pending' 788 | ); 789 | }; 790 | data.progress = function () { 791 | return this._progress; 792 | }; 793 | data.response = function () { 794 | return this._response; 795 | }; 796 | }, 797 | 798 | // Parses the Range header from the server response 799 | // and returns the uploaded bytes: 800 | _getUploadedBytes: function (jqXHR) { 801 | var range = jqXHR.getResponseHeader('Range'), 802 | parts = range && range.split('-'), 803 | upperBytesPos = parts && parts.length > 1 && parseInt(parts[1], 10); 804 | return upperBytesPos && upperBytesPos + 1; 805 | }, 806 | 807 | // Uploads a file in multiple, sequential requests 808 | // by splitting the file up in multiple blob chunks. 809 | // If the second parameter is true, only tests if the file 810 | // should be uploaded in chunks, but does not invoke any 811 | // upload requests: 812 | _chunkedUpload: function (options, testOnly) { 813 | options.uploadedBytes = options.uploadedBytes || 0; 814 | var that = this, 815 | file = options.files[0], 816 | fs = file.size, 817 | ub = options.uploadedBytes, 818 | mcs = options.maxChunkSize || fs, 819 | slice = this._blobSlice, 820 | dfd = $.Deferred(), 821 | promise = dfd.promise(), 822 | jqXHR, 823 | upload; 824 | if ( 825 | !( 826 | this._isXHRUpload(options) && 827 | slice && 828 | (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs) 829 | ) || 830 | options.data 831 | ) { 832 | return false; 833 | } 834 | if (testOnly) { 835 | return true; 836 | } 837 | if (ub >= fs) { 838 | file.error = options.i18n('uploadedBytes'); 839 | return this._getXHRPromise(false, options.context, [ 840 | null, 841 | 'error', 842 | file.error 843 | ]); 844 | } 845 | // The chunk upload method: 846 | upload = function () { 847 | // Clone the options object for each chunk upload: 848 | var o = $.extend({}, options), 849 | currentLoaded = o._progress.loaded; 850 | o.blob = slice.call( 851 | file, 852 | ub, 853 | ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), 854 | file.type 855 | ); 856 | // Store the current chunk size, as the blob itself 857 | // will be dereferenced after data processing: 858 | o.chunkSize = o.blob.size; 859 | // Expose the chunk bytes position range: 860 | o.contentRange = 861 | 'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs; 862 | // Trigger chunkbeforesend to allow form data to be updated for this chunk 863 | that._trigger('chunkbeforesend', null, o); 864 | // Process the upload data (the blob and potential form data): 865 | that._initXHRData(o); 866 | // Add progress listeners for this chunk upload: 867 | that._initProgressListener(o); 868 | jqXHR = ( 869 | (that._trigger('chunksend', null, o) !== false && $.ajax(o)) || 870 | that._getXHRPromise(false, o.context) 871 | ) 872 | .done(function (result, textStatus, jqXHR) { 873 | ub = that._getUploadedBytes(jqXHR) || ub + o.chunkSize; 874 | // Create a progress event if no final progress event 875 | // with loaded equaling total has been triggered 876 | // for this chunk: 877 | if (currentLoaded + o.chunkSize - o._progress.loaded) { 878 | that._onProgress( 879 | $.Event('progress', { 880 | lengthComputable: true, 881 | loaded: ub - o.uploadedBytes, 882 | total: ub - o.uploadedBytes 883 | }), 884 | o 885 | ); 886 | } 887 | options.uploadedBytes = o.uploadedBytes = ub; 888 | o.result = result; 889 | o.textStatus = textStatus; 890 | o.jqXHR = jqXHR; 891 | that._trigger('chunkdone', null, o); 892 | that._trigger('chunkalways', null, o); 893 | if (ub < fs) { 894 | // File upload not yet complete, 895 | // continue with the next chunk: 896 | upload(); 897 | } else { 898 | dfd.resolveWith(o.context, [result, textStatus, jqXHR]); 899 | } 900 | }) 901 | .fail(function (jqXHR, textStatus, errorThrown) { 902 | o.jqXHR = jqXHR; 903 | o.textStatus = textStatus; 904 | o.errorThrown = errorThrown; 905 | that._trigger('chunkfail', null, o); 906 | that._trigger('chunkalways', null, o); 907 | dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]); 908 | }) 909 | .always(function () { 910 | that._deinitProgressListener(o); 911 | }); 912 | }; 913 | this._enhancePromise(promise); 914 | promise.abort = function () { 915 | return jqXHR.abort(); 916 | }; 917 | upload(); 918 | return promise; 919 | }, 920 | 921 | _beforeSend: function (e, data) { 922 | if (this._active === 0) { 923 | // the start callback is triggered when an upload starts 924 | // and no other uploads are currently running, 925 | // equivalent to the global ajaxStart event: 926 | this._trigger('start'); 927 | // Set timer for global bitrate progress calculation: 928 | this._bitrateTimer = new this._BitrateTimer(); 929 | // Reset the global progress values: 930 | this._progress.loaded = this._progress.total = 0; 931 | this._progress.bitrate = 0; 932 | } 933 | // Make sure the container objects for the .response() and 934 | // .progress() methods on the data object are available 935 | // and reset to their initial state: 936 | this._initResponseObject(data); 937 | this._initProgressObject(data); 938 | data._progress.loaded = data.loaded = data.uploadedBytes || 0; 939 | data._progress.total = data.total = this._getTotal(data.files) || 1; 940 | data._progress.bitrate = data.bitrate = 0; 941 | this._active += 1; 942 | // Initialize the global progress values: 943 | this._progress.loaded += data.loaded; 944 | this._progress.total += data.total; 945 | }, 946 | 947 | _onDone: function (result, textStatus, jqXHR, options) { 948 | var total = options._progress.total, 949 | response = options._response; 950 | if (options._progress.loaded < total) { 951 | // Create a progress event if no final progress event 952 | // with loaded equaling total has been triggered: 953 | this._onProgress( 954 | $.Event('progress', { 955 | lengthComputable: true, 956 | loaded: total, 957 | total: total 958 | }), 959 | options 960 | ); 961 | } 962 | response.result = options.result = result; 963 | response.textStatus = options.textStatus = textStatus; 964 | response.jqXHR = options.jqXHR = jqXHR; 965 | this._trigger('done', null, options); 966 | }, 967 | 968 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 969 | var response = options._response; 970 | if (options.recalculateProgress) { 971 | // Remove the failed (error or abort) file upload from 972 | // the global progress calculation: 973 | this._progress.loaded -= options._progress.loaded; 974 | this._progress.total -= options._progress.total; 975 | } 976 | response.jqXHR = options.jqXHR = jqXHR; 977 | response.textStatus = options.textStatus = textStatus; 978 | response.errorThrown = options.errorThrown = errorThrown; 979 | this._trigger('fail', null, options); 980 | }, 981 | 982 | _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 983 | // jqXHRorResult, textStatus and jqXHRorError are added to the 984 | // options object via done and fail callbacks 985 | this._trigger('always', null, options); 986 | }, 987 | 988 | _onSend: function (e, data) { 989 | if (!data.submit) { 990 | this._addConvenienceMethods(e, data); 991 | } 992 | var that = this, 993 | jqXHR, 994 | aborted, 995 | slot, 996 | pipe, 997 | options = that._getAJAXSettings(data), 998 | send = function () { 999 | that._sending += 1; 1000 | // Set timer for bitrate progress calculation: 1001 | options._bitrateTimer = new that._BitrateTimer(); 1002 | jqXHR = 1003 | jqXHR || 1004 | ( 1005 | ((aborted || 1006 | that._trigger( 1007 | 'send', 1008 | $.Event('send', { delegatedEvent: e }), 1009 | options 1010 | ) === false) && 1011 | that._getXHRPromise(false, options.context, aborted)) || 1012 | that._chunkedUpload(options) || 1013 | $.ajax(options) 1014 | ) 1015 | .done(function (result, textStatus, jqXHR) { 1016 | that._onDone(result, textStatus, jqXHR, options); 1017 | }) 1018 | .fail(function (jqXHR, textStatus, errorThrown) { 1019 | that._onFail(jqXHR, textStatus, errorThrown, options); 1020 | }) 1021 | .always(function (jqXHRorResult, textStatus, jqXHRorError) { 1022 | that._deinitProgressListener(options); 1023 | that._onAlways( 1024 | jqXHRorResult, 1025 | textStatus, 1026 | jqXHRorError, 1027 | options 1028 | ); 1029 | that._sending -= 1; 1030 | that._active -= 1; 1031 | if ( 1032 | options.limitConcurrentUploads && 1033 | options.limitConcurrentUploads > that._sending 1034 | ) { 1035 | // Start the next queued upload, 1036 | // that has not been aborted: 1037 | var nextSlot = that._slots.shift(); 1038 | while (nextSlot) { 1039 | if (that._getDeferredState(nextSlot) === 'pending') { 1040 | nextSlot.resolve(); 1041 | break; 1042 | } 1043 | nextSlot = that._slots.shift(); 1044 | } 1045 | } 1046 | if (that._active === 0) { 1047 | // The stop callback is triggered when all uploads have 1048 | // been completed, equivalent to the global ajaxStop event: 1049 | that._trigger('stop'); 1050 | } 1051 | }); 1052 | return jqXHR; 1053 | }; 1054 | this._beforeSend(e, options); 1055 | if ( 1056 | this.options.sequentialUploads || 1057 | (this.options.limitConcurrentUploads && 1058 | this.options.limitConcurrentUploads <= this._sending) 1059 | ) { 1060 | if (this.options.limitConcurrentUploads > 1) { 1061 | slot = $.Deferred(); 1062 | this._slots.push(slot); 1063 | pipe = slot[that._promisePipe](send); 1064 | } else { 1065 | this._sequence = this._sequence[that._promisePipe](send, send); 1066 | pipe = this._sequence; 1067 | } 1068 | // Return the piped Promise object, enhanced with an abort method, 1069 | // which is delegated to the jqXHR object of the current upload, 1070 | // and jqXHR callbacks mapped to the equivalent Promise methods: 1071 | pipe.abort = function () { 1072 | aborted = [undefined, 'abort', 'abort']; 1073 | if (!jqXHR) { 1074 | if (slot) { 1075 | slot.rejectWith(options.context, aborted); 1076 | } 1077 | return send(); 1078 | } 1079 | return jqXHR.abort(); 1080 | }; 1081 | return this._enhancePromise(pipe); 1082 | } 1083 | return send(); 1084 | }, 1085 | 1086 | _onAdd: function (e, data) { 1087 | var that = this, 1088 | result = true, 1089 | options = $.extend({}, this.options, data), 1090 | files = data.files, 1091 | filesLength = files.length, 1092 | limit = options.limitMultiFileUploads, 1093 | limitSize = options.limitMultiFileUploadSize, 1094 | overhead = options.limitMultiFileUploadSizeOverhead, 1095 | batchSize = 0, 1096 | paramName = this._getParamName(options), 1097 | paramNameSet, 1098 | paramNameSlice, 1099 | fileSet, 1100 | i, 1101 | j = 0; 1102 | if (!filesLength) { 1103 | return false; 1104 | } 1105 | if (limitSize && files[0].size === undefined) { 1106 | limitSize = undefined; 1107 | } 1108 | if ( 1109 | !(options.singleFileUploads || limit || limitSize) || 1110 | !this._isXHRUpload(options) 1111 | ) { 1112 | fileSet = [files]; 1113 | paramNameSet = [paramName]; 1114 | } else if (!(options.singleFileUploads || limitSize) && limit) { 1115 | fileSet = []; 1116 | paramNameSet = []; 1117 | for (i = 0; i < filesLength; i += limit) { 1118 | fileSet.push(files.slice(i, i + limit)); 1119 | paramNameSlice = paramName.slice(i, i + limit); 1120 | if (!paramNameSlice.length) { 1121 | paramNameSlice = paramName; 1122 | } 1123 | paramNameSet.push(paramNameSlice); 1124 | } 1125 | } else if (!options.singleFileUploads && limitSize) { 1126 | fileSet = []; 1127 | paramNameSet = []; 1128 | for (i = 0; i < filesLength; i = i + 1) { 1129 | batchSize += files[i].size + overhead; 1130 | if ( 1131 | i + 1 === filesLength || 1132 | batchSize + files[i + 1].size + overhead > limitSize || 1133 | (limit && i + 1 - j >= limit) 1134 | ) { 1135 | fileSet.push(files.slice(j, i + 1)); 1136 | paramNameSlice = paramName.slice(j, i + 1); 1137 | if (!paramNameSlice.length) { 1138 | paramNameSlice = paramName; 1139 | } 1140 | paramNameSet.push(paramNameSlice); 1141 | j = i + 1; 1142 | batchSize = 0; 1143 | } 1144 | } 1145 | } else { 1146 | paramNameSet = paramName; 1147 | } 1148 | data.originalFiles = files; 1149 | $.each(fileSet || files, function (index, element) { 1150 | var newData = $.extend({}, data); 1151 | newData.files = fileSet ? element : [element]; 1152 | newData.paramName = paramNameSet[index]; 1153 | that._initResponseObject(newData); 1154 | that._initProgressObject(newData); 1155 | that._addConvenienceMethods(e, newData); 1156 | result = that._trigger( 1157 | 'add', 1158 | $.Event('add', { delegatedEvent: e }), 1159 | newData 1160 | ); 1161 | return result; 1162 | }); 1163 | return result; 1164 | }, 1165 | 1166 | _replaceFileInput: function (data) { 1167 | var input = data.fileInput, 1168 | inputClone = input.clone(true), 1169 | restoreFocus = input.is(document.activeElement); 1170 | // Add a reference for the new cloned file input to the data argument: 1171 | data.fileInputClone = inputClone; 1172 | $('
    ').append(inputClone)[0].reset(); 1173 | // Detaching allows to insert the fileInput on another form 1174 | // without losing the file input value: 1175 | input.after(inputClone).detach(); 1176 | // If the fileInput had focus before it was detached, 1177 | // restore focus to the inputClone. 1178 | if (restoreFocus) { 1179 | inputClone.trigger('focus'); 1180 | } 1181 | // Avoid memory leaks with the detached file input: 1182 | $.cleanData(input.off('remove')); 1183 | // Replace the original file input element in the fileInput 1184 | // elements set with the clone, which has been copied including 1185 | // event handlers: 1186 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 1187 | if (el === input[0]) { 1188 | return inputClone[0]; 1189 | } 1190 | return el; 1191 | }); 1192 | // If the widget has been initialized on the file input itself, 1193 | // override this.element with the file input clone: 1194 | if (input[0] === this.element[0]) { 1195 | this.element = inputClone; 1196 | } 1197 | }, 1198 | 1199 | _handleFileTreeEntry: function (entry, path) { 1200 | var that = this, 1201 | dfd = $.Deferred(), 1202 | entries = [], 1203 | dirReader, 1204 | errorHandler = function (e) { 1205 | if (e && !e.entry) { 1206 | e.entry = entry; 1207 | } 1208 | // Since $.when returns immediately if one 1209 | // Deferred is rejected, we use resolve instead. 1210 | // This allows valid files and invalid items 1211 | // to be returned together in one set: 1212 | dfd.resolve([e]); 1213 | }, 1214 | successHandler = function (entries) { 1215 | that 1216 | ._handleFileTreeEntries(entries, path + entry.name + '/') 1217 | .done(function (files) { 1218 | dfd.resolve(files); 1219 | }) 1220 | .fail(errorHandler); 1221 | }, 1222 | readEntries = function () { 1223 | dirReader.readEntries(function (results) { 1224 | if (!results.length) { 1225 | successHandler(entries); 1226 | } else { 1227 | entries = entries.concat(results); 1228 | readEntries(); 1229 | } 1230 | }, errorHandler); 1231 | }; 1232 | // eslint-disable-next-line no-param-reassign 1233 | path = path || ''; 1234 | if (entry.isFile) { 1235 | if (entry._file) { 1236 | // Workaround for Chrome bug #149735 1237 | entry._file.relativePath = path; 1238 | dfd.resolve(entry._file); 1239 | } else { 1240 | entry.file(function (file) { 1241 | file.relativePath = path; 1242 | dfd.resolve(file); 1243 | }, errorHandler); 1244 | } 1245 | } else if (entry.isDirectory) { 1246 | dirReader = entry.createReader(); 1247 | readEntries(); 1248 | } else { 1249 | // Return an empty list for file system items 1250 | // other than files or directories: 1251 | dfd.resolve([]); 1252 | } 1253 | return dfd.promise(); 1254 | }, 1255 | 1256 | _handleFileTreeEntries: function (entries, path) { 1257 | var that = this; 1258 | return $.when 1259 | .apply( 1260 | $, 1261 | $.map(entries, function (entry) { 1262 | return that._handleFileTreeEntry(entry, path); 1263 | }) 1264 | ) 1265 | [this._promisePipe](function () { 1266 | return Array.prototype.concat.apply([], arguments); 1267 | }); 1268 | }, 1269 | 1270 | _getDroppedFiles: function (dataTransfer) { 1271 | // eslint-disable-next-line no-param-reassign 1272 | dataTransfer = dataTransfer || {}; 1273 | var items = dataTransfer.items; 1274 | if ( 1275 | items && 1276 | items.length && 1277 | (items[0].webkitGetAsEntry || items[0].getAsEntry) 1278 | ) { 1279 | return this._handleFileTreeEntries( 1280 | $.map(items, function (item) { 1281 | var entry; 1282 | if (item.webkitGetAsEntry) { 1283 | entry = item.webkitGetAsEntry(); 1284 | if (entry) { 1285 | // Workaround for Chrome bug #149735: 1286 | entry._file = item.getAsFile(); 1287 | } 1288 | return entry; 1289 | } 1290 | return item.getAsEntry(); 1291 | }) 1292 | ); 1293 | } 1294 | return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise(); 1295 | }, 1296 | 1297 | _getSingleFileInputFiles: function (fileInput) { 1298 | // eslint-disable-next-line no-param-reassign 1299 | fileInput = $(fileInput); 1300 | var entries = fileInput.prop('entries'), 1301 | files, 1302 | value; 1303 | if (entries && entries.length) { 1304 | return this._handleFileTreeEntries(entries); 1305 | } 1306 | files = $.makeArray(fileInput.prop('files')); 1307 | if (!files.length) { 1308 | value = fileInput.prop('value'); 1309 | if (!value) { 1310 | return $.Deferred().resolve([]).promise(); 1311 | } 1312 | // If the files property is not available, the browser does not 1313 | // support the File API and we add a pseudo File object with 1314 | // the input value as name with path information removed: 1315 | files = [{ name: value.replace(/^.*\\/, '') }]; 1316 | } else if (files[0].name === undefined && files[0].fileName) { 1317 | // File normalization for Safari 4 and Firefox 3: 1318 | $.each(files, function (index, file) { 1319 | file.name = file.fileName; 1320 | file.size = file.fileSize; 1321 | }); 1322 | } 1323 | return $.Deferred().resolve(files).promise(); 1324 | }, 1325 | 1326 | _getFileInputFiles: function (fileInput) { 1327 | if (!(fileInput instanceof $) || fileInput.length === 1) { 1328 | return this._getSingleFileInputFiles(fileInput); 1329 | } 1330 | return $.when 1331 | .apply($, $.map(fileInput, this._getSingleFileInputFiles)) 1332 | [this._promisePipe](function () { 1333 | return Array.prototype.concat.apply([], arguments); 1334 | }); 1335 | }, 1336 | 1337 | _onChange: function (e) { 1338 | var that = this, 1339 | data = { 1340 | fileInput: $(e.target), 1341 | form: $(e.target.form) 1342 | }; 1343 | this._getFileInputFiles(data.fileInput).always(function (files) { 1344 | data.files = files; 1345 | if (that.options.replaceFileInput) { 1346 | that._replaceFileInput(data); 1347 | } 1348 | if ( 1349 | that._trigger( 1350 | 'change', 1351 | $.Event('change', { delegatedEvent: e }), 1352 | data 1353 | ) !== false 1354 | ) { 1355 | that._onAdd(e, data); 1356 | } 1357 | }); 1358 | }, 1359 | 1360 | _onPaste: function (e) { 1361 | var items = 1362 | e.originalEvent && 1363 | e.originalEvent.clipboardData && 1364 | e.originalEvent.clipboardData.items, 1365 | data = { files: [] }; 1366 | if (items && items.length) { 1367 | $.each(items, function (index, item) { 1368 | var file = item.getAsFile && item.getAsFile(); 1369 | if (file) { 1370 | data.files.push(file); 1371 | } 1372 | }); 1373 | if ( 1374 | this._trigger( 1375 | 'paste', 1376 | $.Event('paste', { delegatedEvent: e }), 1377 | data 1378 | ) !== false 1379 | ) { 1380 | this._onAdd(e, data); 1381 | } 1382 | } 1383 | }, 1384 | 1385 | _onDrop: function (e) { 1386 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1387 | var that = this, 1388 | dataTransfer = e.dataTransfer, 1389 | data = {}; 1390 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 1391 | e.preventDefault(); 1392 | this._getDroppedFiles(dataTransfer).always(function (files) { 1393 | data.files = files; 1394 | if ( 1395 | that._trigger( 1396 | 'drop', 1397 | $.Event('drop', { delegatedEvent: e }), 1398 | data 1399 | ) !== false 1400 | ) { 1401 | that._onAdd(e, data); 1402 | } 1403 | }); 1404 | } 1405 | }, 1406 | 1407 | _onDragOver: getDragHandler('dragover'), 1408 | 1409 | _onDragEnter: getDragHandler('dragenter'), 1410 | 1411 | _onDragLeave: getDragHandler('dragleave'), 1412 | 1413 | _initEventHandlers: function () { 1414 | if (this._isXHRUpload(this.options)) { 1415 | this._on(this.options.dropZone, { 1416 | dragover: this._onDragOver, 1417 | drop: this._onDrop, 1418 | // event.preventDefault() on dragenter is required for IE10+: 1419 | dragenter: this._onDragEnter, 1420 | // dragleave is not required, but added for completeness: 1421 | dragleave: this._onDragLeave 1422 | }); 1423 | this._on(this.options.pasteZone, { 1424 | paste: this._onPaste 1425 | }); 1426 | } 1427 | if ($.support.fileInput) { 1428 | this._on(this.options.fileInput, { 1429 | change: this._onChange 1430 | }); 1431 | } 1432 | }, 1433 | 1434 | _destroyEventHandlers: function () { 1435 | this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); 1436 | this._off(this.options.pasteZone, 'paste'); 1437 | this._off(this.options.fileInput, 'change'); 1438 | }, 1439 | 1440 | _destroy: function () { 1441 | this._destroyEventHandlers(); 1442 | }, 1443 | 1444 | _setOption: function (key, value) { 1445 | var reinit = $.inArray(key, this._specialOptions) !== -1; 1446 | if (reinit) { 1447 | this._destroyEventHandlers(); 1448 | } 1449 | this._super(key, value); 1450 | if (reinit) { 1451 | this._initSpecialOptions(); 1452 | this._initEventHandlers(); 1453 | } 1454 | }, 1455 | 1456 | _initSpecialOptions: function () { 1457 | var options = this.options; 1458 | if (options.fileInput === undefined) { 1459 | options.fileInput = this.element.is('input[type="file"]') 1460 | ? this.element 1461 | : this.element.find('input[type="file"]'); 1462 | } else if (!(options.fileInput instanceof $)) { 1463 | options.fileInput = $(options.fileInput); 1464 | } 1465 | if (!(options.dropZone instanceof $)) { 1466 | options.dropZone = $(options.dropZone); 1467 | } 1468 | if (!(options.pasteZone instanceof $)) { 1469 | options.pasteZone = $(options.pasteZone); 1470 | } 1471 | }, 1472 | 1473 | _getRegExp: function (str) { 1474 | var parts = str.split('/'), 1475 | modifiers = parts.pop(); 1476 | parts.shift(); 1477 | return new RegExp(parts.join('/'), modifiers); 1478 | }, 1479 | 1480 | _isRegExpOption: function (key, value) { 1481 | return ( 1482 | key !== 'url' && 1483 | $.type(value) === 'string' && 1484 | /^\/.*\/[igm]{0,3}$/.test(value) 1485 | ); 1486 | }, 1487 | 1488 | _initDataAttributes: function () { 1489 | var that = this, 1490 | options = this.options, 1491 | data = this.element.data(); 1492 | // Initialize options set via HTML5 data-attributes: 1493 | $.each(this.element[0].attributes, function (index, attr) { 1494 | var key = attr.name.toLowerCase(), 1495 | value; 1496 | if (/^data-/.test(key)) { 1497 | // Convert hyphen-ated key to camelCase: 1498 | key = key.slice(5).replace(/-[a-z]/g, function (str) { 1499 | return str.charAt(1).toUpperCase(); 1500 | }); 1501 | value = data[key]; 1502 | if (that._isRegExpOption(key, value)) { 1503 | value = that._getRegExp(value); 1504 | } 1505 | options[key] = value; 1506 | } 1507 | }); 1508 | }, 1509 | 1510 | _create: function () { 1511 | this._initDataAttributes(); 1512 | this._initSpecialOptions(); 1513 | this._slots = []; 1514 | this._sequence = this._getXHRPromise(true); 1515 | this._sending = this._active = 0; 1516 | this._initProgressObject(this); 1517 | this._initEventHandlers(); 1518 | }, 1519 | 1520 | // This method is exposed to the widget API and allows to query 1521 | // the number of active uploads: 1522 | active: function () { 1523 | return this._active; 1524 | }, 1525 | 1526 | // This method is exposed to the widget API and allows to query 1527 | // the widget upload progress. 1528 | // It returns an object with loaded, total and bitrate properties 1529 | // for the running uploads: 1530 | progress: function () { 1531 | return this._progress; 1532 | }, 1533 | 1534 | // This method is exposed to the widget API and allows adding files 1535 | // using the fileupload API. The data parameter accepts an object which 1536 | // must have a files property and can contain additional options: 1537 | // .fileupload('add', {files: filesList}); 1538 | add: function (data) { 1539 | var that = this; 1540 | if (!data || this.options.disabled) { 1541 | return; 1542 | } 1543 | if (data.fileInput && !data.files) { 1544 | this._getFileInputFiles(data.fileInput).always(function (files) { 1545 | data.files = files; 1546 | that._onAdd(null, data); 1547 | }); 1548 | } else { 1549 | data.files = $.makeArray(data.files); 1550 | this._onAdd(null, data); 1551 | } 1552 | }, 1553 | 1554 | // This method is exposed to the widget API and allows sending files 1555 | // using the fileupload API. The data parameter accepts an object which 1556 | // must have a files or fileInput property and can contain additional options: 1557 | // .fileupload('send', {files: filesList}); 1558 | // The method returns a Promise object for the file upload call. 1559 | send: function (data) { 1560 | if (data && !this.options.disabled) { 1561 | if (data.fileInput && !data.files) { 1562 | var that = this, 1563 | dfd = $.Deferred(), 1564 | promise = dfd.promise(), 1565 | jqXHR, 1566 | aborted; 1567 | promise.abort = function () { 1568 | aborted = true; 1569 | if (jqXHR) { 1570 | return jqXHR.abort(); 1571 | } 1572 | dfd.reject(null, 'abort', 'abort'); 1573 | return promise; 1574 | }; 1575 | this._getFileInputFiles(data.fileInput).always(function (files) { 1576 | if (aborted) { 1577 | return; 1578 | } 1579 | if (!files.length) { 1580 | dfd.reject(); 1581 | return; 1582 | } 1583 | data.files = files; 1584 | jqXHR = that._onSend(null, data); 1585 | jqXHR.then( 1586 | function (result, textStatus, jqXHR) { 1587 | dfd.resolve(result, textStatus, jqXHR); 1588 | }, 1589 | function (jqXHR, textStatus, errorThrown) { 1590 | dfd.reject(jqXHR, textStatus, errorThrown); 1591 | } 1592 | ); 1593 | }); 1594 | return this._enhancePromise(promise); 1595 | } 1596 | data.files = $.makeArray(data.files); 1597 | if (data.files.length) { 1598 | return this._onSend(null, data); 1599 | } 1600 | } 1601 | return this._getXHRPromise(false, data && data.context); 1602 | } 1603 | }); 1604 | }); 1605 | -------------------------------------------------------------------------------- /src/ParameterProvider.php: -------------------------------------------------------------------------------- 1 | . 20 | * 21 | * @file 22 | * @ingroup SimpleBatchUpload 23 | */ 24 | 25 | namespace MediaWiki\Extension\SimpleBatchUpload; 26 | 27 | use Message; 28 | 29 | /** 30 | * Class ParameterProvider 31 | * 32 | * @package SimpleBatchUpload 33 | */ 34 | class ParameterProvider { 35 | 36 | const IDX_TEMPLATENAME = 0; 37 | const IDX_TEMPLATEPARAMETERS = 1; 38 | const IDX_COMMENT = 2; 39 | const IDX_SPECIALPAGETITLE = 3; 40 | 41 | private $templateName; 42 | private $parameters = null; 43 | 44 | /** 45 | * @param string|null $templateName 46 | */ 47 | public function __construct( $templateName ) { 48 | $this->templateName = $templateName ? $templateName : ''; 49 | } 50 | 51 | public function getUploadPageText(): string { 52 | 53 | if ( $this->templateName === '' ) { 54 | return ''; 55 | } 56 | 57 | return '{{' . $this->getParameter( self::IDX_TEMPLATENAME ) . $this->getParameter( self::IDX_TEMPLATEPARAMETERS ) . '}}'; 58 | } 59 | 60 | private function getEscapedParameter( int $key ): string { 61 | return $this->escape( $this->getParameter( $key ) ); 62 | } 63 | 64 | private function escape( string $text ): string { 65 | return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8', false ); 66 | } 67 | 68 | private function getParameter( int $key ): string { 69 | if ( $this->parameters === null ) { 70 | $this->populateParameters(); 71 | } 72 | return $this->parameters[ $key ]; 73 | } 74 | 75 | private function populateParameters() { 76 | if ( $this->templateName === '' || $this->populateParametersFromKey() === false ) { 77 | $this->populateParametersFromDefaults(); 78 | } 79 | } 80 | 81 | private function populateParametersFromKey() { 82 | $paramMsg = Message::newFromKey( 'simplebatchupload-parameters' ); 83 | 84 | if ( $paramMsg->exists() ) { 85 | 86 | $paramLines = explode( "\n", $paramMsg->plain() ); 87 | $paramSet = array_map( [ $this, 'parseParamLine' ], $paramLines ); 88 | $paramMap = array_combine( array_column( $paramSet, 0 ), $paramSet ); 89 | 90 | if ( array_key_exists( $this->templateName, $paramMap ) ) { 91 | $this->setParameters( $this->templateName, '', $paramMap[ $this->templateName ][ 1 ], $paramMap[ $this->templateName ][ 2 ] ); 92 | return true; 93 | } 94 | } 95 | return false; 96 | } 97 | 98 | private function populateParametersFromDefaults() { 99 | $this->setParameters( $this->templateName, '', Message::newFromKey( 'simplebatchupload-comment' )->text(), Message::newFromKey( 'batchupload' )->text() ); 100 | } 101 | 102 | /** 103 | * @param string $templateName 104 | * @param string $templateParameters 105 | * @param string $uploadComment 106 | * @param string $specialPageTitle 107 | */ 108 | private function setParameters( $templateName, $templateParameters, $uploadComment, $specialPageTitle ) { 109 | $this->parameters = [ 110 | self::IDX_TEMPLATENAME => $templateName, 111 | self::IDX_TEMPLATEPARAMETERS => $templateParameters, 112 | self::IDX_COMMENT => $uploadComment, 113 | self::IDX_SPECIALPAGETITLE => $specialPageTitle, 114 | ]; 115 | } 116 | 117 | public function getEscapedUploadComment(): string { 118 | return $this->getEscapedParameter( self::IDX_COMMENT ); 119 | } 120 | 121 | public function getSpecialPageTitle(): string { 122 | return $this->getParameter( self::IDX_SPECIALPAGETITLE ); 123 | } 124 | 125 | public function addTemplateParameter( string $parameter ) { 126 | 127 | if ( $this->parameters === null ) { 128 | $this->populateParameters(); 129 | } 130 | 131 | $this->parameters[ self::IDX_TEMPLATEPARAMETERS ] .= '|' . $parameter; 132 | } 133 | 134 | /** 135 | * @param string $paramLine 136 | * @return string[] 137 | */ 138 | private function parseParamLine( string $paramLine ): array { 139 | return array_replace( [ '', '', '' ], array_map( 'trim', explode( '|', $paramLine, 3 ) ) ); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/SimpleBatchUpload.alias.php: -------------------------------------------------------------------------------- 1 | . 20 | * 21 | * @file 22 | * @ingroup ExtensionManager 23 | */ 24 | 25 | $specialPageAliases = []; 26 | 27 | /** English 28 | */ 29 | $specialPageAliases['en'] = [ 30 | 'BatchUpload' => [ 'BatchUpload' ], 31 | ]; 32 | -------------------------------------------------------------------------------- /src/SimpleBatchUpload.magic.php: -------------------------------------------------------------------------------- 1 | . 20 | * 21 | * @file 22 | * @ingroup ExtensionManager 23 | */ 24 | 25 | $magicWords = []; 26 | 27 | /** English 28 | */ 29 | $magicWords['en'] = [ 30 | 'batchupload' => [ 0, 'batchupload' ], 31 | ]; -------------------------------------------------------------------------------- /src/SimpleBatchUpload.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @file 20 | * @ingroup SimpleBatchUpload 21 | */ 22 | 23 | namespace MediaWiki\Extension\SimpleBatchUpload; 24 | 25 | use MediaWiki\MediaWikiServices; 26 | use Parser; 27 | 28 | /** 29 | * Class ExtensionManager 30 | * 31 | * @package SimpleBatchUpload 32 | */ 33 | class SimpleBatchUpload { 34 | 35 | /** 36 | * @param \Parser $parser 37 | * 38 | * @return bool 39 | * @throws \MWException 40 | */ 41 | public static function registerParserFunction( &$parser ) { 42 | $parser->setFunctionHook( 'batchupload', [ new UploadButtonRenderer(), 'renderParserFunction' ], Parser::SFH_OBJECT_ARGS ); 43 | return true; 44 | } 45 | 46 | /** 47 | * @param array &$vars 48 | * @param \OutputPage $out 49 | */ 50 | public static function onMakeGlobalVariablesScript( &$vars, $out ) { 51 | $vars['simpleBatchUploadMaxFilesPerBatch'] = MediaWikiServices::getInstance()->getMainConfig()->get( 'SimpleBatchUploadMaxFilesPerBatch' ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/SpecialBatchUpload.php: -------------------------------------------------------------------------------- 1 | . 20 | * 21 | * @file 22 | * @ingroup SimpleBatchUpload 23 | */ 24 | 25 | namespace MediaWiki\Extension\SimpleBatchUpload; 26 | 27 | use SpecialPage; 28 | 29 | /** 30 | * Class SpecialBatchUpload 31 | * 32 | * @package SimpleBatchUpload 33 | */ 34 | class SpecialBatchUpload extends SpecialPage { 35 | 36 | /** 37 | * @param string $name Name of the special page, as seen in links and URLs 38 | * @param string $restriction User right required, e.g. "block" or "delete" 39 | * @param bool $listed Whether the page is listed in Special:Specialpages 40 | */ 41 | public function __construct( $name = '', $restriction = '', $listed = true ) { 42 | parent::__construct( 'BatchUpload', 'upload', $listed ); 43 | } 44 | 45 | /** 46 | * Under which header this special page is listed in Special:SpecialPages 47 | * See messages 'specialpages-group-*' for valid names 48 | * This method defaults to group 'other' 49 | * 50 | * @return string 51 | */ 52 | protected function getGroupName() { 53 | return 'media'; 54 | } 55 | 56 | /** 57 | * @param null|string $subpage 58 | * @throws \MWException 59 | */ 60 | public function execute( $subpage ) { 61 | 62 | $this->setHeaders(); 63 | $this->checkPermissions(); 64 | 65 | $this->addPageContentToOutput( $subpage ); 66 | } 67 | 68 | /** 69 | * @param string|null $subpage 70 | */ 71 | private function addPageContentToOutput( $subpage ) { 72 | $renderer = new UploadButtonRenderer(); 73 | $renderer->renderSpecialPage( $this, $subpage ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/UploadButtonRenderer.php: -------------------------------------------------------------------------------- 1 | . 20 | * 21 | * @file 22 | * @ingroup SimpleBatchUpload 23 | */ 24 | 25 | namespace MediaWiki\Extension\SimpleBatchUpload; 26 | use Parser; 27 | use PPFrame; 28 | use Html; 29 | 30 | /** 31 | * Class UploadButtonRenderer 32 | * @package SimpleBatchUpload 33 | */ 34 | class UploadButtonRenderer { 35 | 36 | /** 37 | * @param Parser $parser 38 | * @param PPFrame $frame 39 | * @param $args 40 | * @return array 41 | */ 42 | public function renderParserFunction( Parser $parser, PPFrame $frame, $args ): array { 43 | $args = array_map( [ $frame, 'expand' ], $args ); 44 | $output = $parser->getOutput(); 45 | 46 | $html = $this->renderUploadButton( $args, $output ); 47 | 48 | return [ $html, 'isHTML' => true, 'noparse' => true, 'nowiki' => false ]; 49 | } 50 | 51 | /** 52 | * @param SpecialBatchUpload $specialPage 53 | * @param string $templateName 54 | */ 55 | public function renderSpecialPage( SpecialBatchUpload $specialPage, $templateName ) { 56 | $args = [ $templateName ]; 57 | $output = $specialPage->getOutput(); 58 | 59 | $html = $this->renderUploadButton( $args, $output ); 60 | 61 | $output->addHTML( $html ); 62 | } 63 | 64 | /** 65 | * @param string[] $args 66 | * @param \ParserOutput | \OutputPage $output 67 | * @return string 68 | */ 69 | protected function renderUploadButton( $args, $output ) { 70 | $paramProvider = $this->prepareParameterProvider( $args ); 71 | 72 | $this->addModulesToOutput( $output ); 73 | 74 | if ( method_exists( $output, 'setPageTitle' ) ) { 75 | $output->setPageTitle( $paramProvider->getSpecialPageTitle() ); 76 | } 77 | 78 | return $this->getHtml( $paramProvider ); 79 | } 80 | 81 | /** 82 | * @param $paramProvider 83 | * @return string 84 | */ 85 | protected function getHtml( ParameterProvider $paramProvider ) { 86 | 87 | $escapedUploadComment = $paramProvider->getEscapedUploadComment(); 88 | $uploadPageText = $paramProvider->getUploadPageText(); 89 | 90 | return 91 | 92 | '
    ' . 93 | '
    '. 98 | ' ' . 99 | ' ' . 100 | '' . \Message::newFromKey( 'simplebatchupload-buttonlabel' )->escaped() . ' ' . 101 | '' . 102 | ' ' . 106 | '
      ' . 107 | '
      '; 108 | } 109 | 110 | /** 111 | * @param \ParserOutput | \OutputPage $output 112 | */ 113 | protected function addModulesToOutput( $output ) { 114 | $output->addModules( [ 'ext.SimpleBatchUpload' ] ); 115 | $output->addModuleStyles( [ 'ext.SimpleBatchUpload', 'ext.SimpleBatchUpload.jquery-file-upload' ] ); 116 | } 117 | 118 | /** 119 | * @param string[] $args 120 | * @return ParameterProvider 121 | */ 122 | protected function prepareParameterProvider( $args ): ParameterProvider { 123 | 124 | $templateName = $args[ 0 ]; 125 | 126 | $paramProvider = new ParameterProvider( $templateName ); 127 | 128 | if ( $templateName !== '' ) { 129 | array_shift( $args ); 130 | foreach ( $args as $node ) { 131 | $paramProvider->addTemplateParameter( $node ); 132 | } 133 | } 134 | return $paramProvider; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /tests/phpunit/SimpleBatchUploadTest.php: -------------------------------------------------------------------------------- 1 | . 18 | * 19 | * @file 20 | * @ingroup SimpleBatchUpload 21 | */ 22 | 23 | namespace MediaWiki\Extension\SimpleBatchUpload\Tests; 24 | 25 | use OutputPage; 26 | use Parser; 27 | use MediaWiki\Extension\SimpleBatchUpload\SimpleBatchUpload; 28 | 29 | /** 30 | * @covers \MediaWiki\Extension\SimpleBatchUpload\SimpleBatchUpload 31 | * @group SimpleBatchUpload 32 | * 33 | * @since 1.5 34 | */ 35 | class SimpleBatchUploadTest extends \PHPUnit\Framework\TestCase { 36 | 37 | public function testCanConstruct() { 38 | 39 | $this->assertInstanceOf( 40 | '\MediaWiki\Extension\SimpleBatchUpload\SimpleBatchUpload', 41 | new SimpleBatchUpload() 42 | ); 43 | } 44 | 45 | public function testRegisterParserFunction() { 46 | 47 | $parser = $this->getMockBuilder( Parser::class ) 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | 51 | $parser->expects( $this->once() ) 52 | ->method( 'setFunctionHook' ) 53 | ->with( 54 | $this->equalTo( 'batchupload' ), 55 | $this->callback( function ( $param ) { 56 | return is_callable( $param ); 57 | } ), 58 | $this->equalTo( Parser::SFH_OBJECT_ARGS ) ) 59 | ->willReturn( null ); 60 | 61 | $sbu = new SimpleBatchUpload(); 62 | $sbu->registerParserFunction( $parser ); 63 | } 64 | 65 | public function testOnMakeGlobalVariablesScript() { 66 | $vars = []; 67 | $out = $this->getMockBuilder( OutputPage::class ) 68 | ->disableOriginalConstructor() 69 | ->getMock(); 70 | 71 | $sbu = new SimpleBatchUpload(); 72 | $sbu->onMakeGlobalVariablesScript( $vars, $out ); 73 | 74 | $this->assertArrayHasKey( 'simpleBatchUploadMaxFilesPerBatch', $vars ); 75 | } 76 | 77 | /** 78 | * @param $configuration 79 | */ 80 | public function assertJsonConfiguration( $configuration ) { 81 | $this->assertArrayHasKey( 'wgSimpleBatchUploadMaxFilesPerBatch', $configuration ); 82 | } 83 | 84 | } 85 | --------------------------------------------------------------------------------