├── .editorconfig ├── .github └── workflows │ └── php-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── api └── index.php ├── composer.json ├── composer.lock ├── config-default.ini ├── config-example.ini ├── includes ├── APIHelpExamples.php ├── APIHelpObject.php ├── Config.php ├── CountQuery.php ├── DatabaseFactory.php ├── Footer.php ├── Form.php ├── Global.php ├── HtmlProducer.php ├── JsLoader.php ├── JsonProducer.php ├── LinkCount.php ├── PageLookupWidget.php ├── ProjectLookupWidget.php ├── ProjectPrefixSearch.php └── Title.php ├── index.php ├── js └── index.php ├── output └── index.php ├── package-lock.json ├── package.json ├── projects └── index.php ├── robots.txt ├── static ├── NamespaceLookupWidget.js ├── PageLookupWidget.js ├── ProjectLookupWidget.js ├── icon.png ├── index.css └── index.js └── tests ├── LinkCountTest.php ├── TestDatabaseFactory.php ├── TitleTest.php ├── createdb.php └── createdb.sql /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = tab 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/workflows/php-tests.yml: -------------------------------------------------------------------------------- 1 | name: PHP Tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | services: 17 | redis: 18 | image: redis 19 | options: >- 20 | --health-cmd "redis-cli ping" 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 6379:6379 26 | 27 | steps: 28 | - name: Check out repository code 29 | uses: actions/checkout@v3 30 | 31 | - name: Start MySQL 32 | run: | 33 | sudo /etc/init.d/mysql start 34 | 35 | - name: Cache Composer packages 36 | id: composer-cache 37 | uses: actions/cache@v3 38 | with: 39 | path: vendor 40 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-php- 43 | 44 | - name: Install dependencies 45 | run: composer install --prefer-dist --no-progress 46 | 47 | - name: Write to config file 48 | run: | 49 | echo test-db-password = root > config.ini 50 | 51 | - name: Run test suite 52 | run: composer run-script test-lin 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | desktop.ini 2 | vendor 3 | node_modules 4 | config.ini 5 | .phpunit.result.cache 6 | google*.html 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linkcount 2 | 3 | **linkcount** is a web program that shows the number of links to any page in a Wikimedia project. It is hosted at . 4 | 5 | ## Installing 6 | 7 | To run you will need to install the PHP dependencies using `composer install` and the Node JS dependencies using `npm install`. 8 | 9 | ## Config 10 | 11 | Copy the file `config-example.ini` to `config.ini` and fill in the fields with values from your `replica.my.cnf` file. 12 | 13 | ## Testing 14 | 15 | Test are run using `composer test-win` on windows and `composer test-lin` on linux etc. When testing, the `linkcounttest` table is created. You can also run `composer createdb` to create the `linkcounttest` table for manual testing from a browser. When manual testing, `en.wikipedia.org` is linked to the `linkcounttest` database. Wikis with a url starting with `e` are also added to the `wiki` table to allow for testing of the project input autocomplete. 16 | -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | getJson(); 9 | 10 | exit; 11 | } 12 | 13 | $description = "API for getting the number of links to any page on any Mediawiki project." 14 | 15 | ?> 16 | 17 | 18 | 19 | Link Count API 20 | 21 | 22 | 23 | 49 | 50 | 51 |

Link Count API

52 |

53 |

Request

54 | getHtml(); ?> 59 |

Response

60 | getHtml(); ?> 67 |

LinkCountObject

68 | getHtml(); ?> 73 |

Error Response

74 | getHtml(); ?> 77 |

Examples

78 | getHtml(); ?> 85 | getHTML(); ?> 86 | 87 | 88 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "oojs/oojs-ui": "^v0.48.2" 4 | }, 5 | "scripts": { 6 | "test-win": "vendor\\bin\\phpunit.bat tests", 7 | "test-lin": "./vendor/bin/phpunit tests", 8 | "createdb": "php tests/createdb.php" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "~9" 12 | }, 13 | "autoload": { 14 | "classmap": [ 15 | "includes" 16 | ], 17 | "files": [ 18 | "includes/Global.php" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "480dd153806a3efe63ec2e5c082f3f02", 8 | "packages": [ 9 | { 10 | "name": "oojs/oojs-ui", 11 | "version": "v0.48.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/wikimedia/oojs-ui.git", 15 | "reference": "1730312010e63b0b729ce9e773ff60666efd7abe" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/wikimedia/oojs-ui/zipball/1730312010e63b0b729ce9e773ff60666efd7abe", 20 | "reference": "1730312010e63b0b729ce9e773ff60666efd7abe", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.4.3" 25 | }, 26 | "require-dev": { 27 | "mediawiki/mediawiki-codesniffer": "41.0.0", 28 | "mediawiki/mediawiki-phan-config": "0.12.1", 29 | "mediawiki/minus-x": "1.1.1", 30 | "php-parallel-lint/php-console-highlighter": "1.0.0", 31 | "php-parallel-lint/php-parallel-lint": "1.3.2", 32 | "phpunit/phpunit": "9.5.28" 33 | }, 34 | "type": "library", 35 | "autoload": { 36 | "classmap": [ 37 | "php/" 38 | ] 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "MIT" 43 | ], 44 | "authors": [ 45 | { 46 | "name": "Bartosz Dziewoński", 47 | "email": "matma.rex@gmail.com" 48 | }, 49 | { 50 | "name": "Ed Sanders", 51 | "email": "esanders@wikimedia.org" 52 | }, 53 | { 54 | "name": "James D. Forrester", 55 | "email": "jforrester@wikimedia.org" 56 | }, 57 | { 58 | "name": "Kirsten Menger-Anderson", 59 | "email": "kmenger@wikimedia.org" 60 | }, 61 | { 62 | "name": "Kunal Mehta", 63 | "email": "legoktm@gmail.com" 64 | }, 65 | { 66 | "name": "Moriel Schottlender", 67 | "email": "mschottlender@wikimedia.org" 68 | }, 69 | { 70 | "name": "Prateek Saxena", 71 | "email": "prtksxna@gmail.com" 72 | }, 73 | { 74 | "name": "Roan Kattouw", 75 | "email": "roan.kattouw@gmail.com" 76 | }, 77 | { 78 | "name": "Thiemo Kreuz", 79 | "email": "thiemo.kreuz@wikimedia.de" 80 | }, 81 | { 82 | "name": "Timo Tijhof", 83 | "email": "krinklemail@gmail.com" 84 | }, 85 | { 86 | "name": "Trevor Parscal", 87 | "email": "trevorparscal@gmail.com" 88 | }, 89 | { 90 | "name": "Volker E.", 91 | "email": "volker.e@wikimedia.org" 92 | } 93 | ], 94 | "description": "Provides library of common widgets, layouts, and windows.", 95 | "homepage": "https://www.mediawiki.org/wiki/OOUI", 96 | "support": { 97 | "source": "https://github.com/wikimedia/oojs-ui/tree/v0.48.2" 98 | }, 99 | "time": "2023-10-24T20:09:40+00:00" 100 | } 101 | ], 102 | "packages-dev": [ 103 | { 104 | "name": "doctrine/instantiator", 105 | "version": "1.5.0", 106 | "source": { 107 | "type": "git", 108 | "url": "https://github.com/doctrine/instantiator.git", 109 | "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" 110 | }, 111 | "dist": { 112 | "type": "zip", 113 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", 114 | "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", 115 | "shasum": "" 116 | }, 117 | "require": { 118 | "php": "^7.1 || ^8.0" 119 | }, 120 | "require-dev": { 121 | "doctrine/coding-standard": "^9 || ^11", 122 | "ext-pdo": "*", 123 | "ext-phar": "*", 124 | "phpbench/phpbench": "^0.16 || ^1", 125 | "phpstan/phpstan": "^1.4", 126 | "phpstan/phpstan-phpunit": "^1", 127 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 128 | "vimeo/psalm": "^4.30 || ^5.4" 129 | }, 130 | "type": "library", 131 | "autoload": { 132 | "psr-4": { 133 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 134 | } 135 | }, 136 | "notification-url": "https://packagist.org/downloads/", 137 | "license": [ 138 | "MIT" 139 | ], 140 | "authors": [ 141 | { 142 | "name": "Marco Pivetta", 143 | "email": "ocramius@gmail.com", 144 | "homepage": "https://ocramius.github.io/" 145 | } 146 | ], 147 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 148 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 149 | "keywords": [ 150 | "constructor", 151 | "instantiate" 152 | ], 153 | "support": { 154 | "issues": "https://github.com/doctrine/instantiator/issues", 155 | "source": "https://github.com/doctrine/instantiator/tree/1.5.0" 156 | }, 157 | "funding": [ 158 | { 159 | "url": "https://www.doctrine-project.org/sponsorship.html", 160 | "type": "custom" 161 | }, 162 | { 163 | "url": "https://www.patreon.com/phpdoctrine", 164 | "type": "patreon" 165 | }, 166 | { 167 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 168 | "type": "tidelift" 169 | } 170 | ], 171 | "time": "2022-12-30T00:15:36+00:00" 172 | }, 173 | { 174 | "name": "myclabs/deep-copy", 175 | "version": "1.11.1", 176 | "source": { 177 | "type": "git", 178 | "url": "https://github.com/myclabs/DeepCopy.git", 179 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" 180 | }, 181 | "dist": { 182 | "type": "zip", 183 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 184 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 185 | "shasum": "" 186 | }, 187 | "require": { 188 | "php": "^7.1 || ^8.0" 189 | }, 190 | "conflict": { 191 | "doctrine/collections": "<1.6.8", 192 | "doctrine/common": "<2.13.3 || >=3,<3.2.2" 193 | }, 194 | "require-dev": { 195 | "doctrine/collections": "^1.6.8", 196 | "doctrine/common": "^2.13.3 || ^3.2.2", 197 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 198 | }, 199 | "type": "library", 200 | "autoload": { 201 | "files": [ 202 | "src/DeepCopy/deep_copy.php" 203 | ], 204 | "psr-4": { 205 | "DeepCopy\\": "src/DeepCopy/" 206 | } 207 | }, 208 | "notification-url": "https://packagist.org/downloads/", 209 | "license": [ 210 | "MIT" 211 | ], 212 | "description": "Create deep copies (clones) of your objects", 213 | "keywords": [ 214 | "clone", 215 | "copy", 216 | "duplicate", 217 | "object", 218 | "object graph" 219 | ], 220 | "support": { 221 | "issues": "https://github.com/myclabs/DeepCopy/issues", 222 | "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" 223 | }, 224 | "funding": [ 225 | { 226 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 227 | "type": "tidelift" 228 | } 229 | ], 230 | "time": "2023-03-08T13:26:56+00:00" 231 | }, 232 | { 233 | "name": "nikic/php-parser", 234 | "version": "v4.17.1", 235 | "source": { 236 | "type": "git", 237 | "url": "https://github.com/nikic/PHP-Parser.git", 238 | "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" 239 | }, 240 | "dist": { 241 | "type": "zip", 242 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", 243 | "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", 244 | "shasum": "" 245 | }, 246 | "require": { 247 | "ext-tokenizer": "*", 248 | "php": ">=7.0" 249 | }, 250 | "require-dev": { 251 | "ircmaxell/php-yacc": "^0.0.7", 252 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" 253 | }, 254 | "bin": [ 255 | "bin/php-parse" 256 | ], 257 | "type": "library", 258 | "extra": { 259 | "branch-alias": { 260 | "dev-master": "4.9-dev" 261 | } 262 | }, 263 | "autoload": { 264 | "psr-4": { 265 | "PhpParser\\": "lib/PhpParser" 266 | } 267 | }, 268 | "notification-url": "https://packagist.org/downloads/", 269 | "license": [ 270 | "BSD-3-Clause" 271 | ], 272 | "authors": [ 273 | { 274 | "name": "Nikita Popov" 275 | } 276 | ], 277 | "description": "A PHP parser written in PHP", 278 | "keywords": [ 279 | "parser", 280 | "php" 281 | ], 282 | "support": { 283 | "issues": "https://github.com/nikic/PHP-Parser/issues", 284 | "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" 285 | }, 286 | "time": "2023-08-13T19:53:39+00:00" 287 | }, 288 | { 289 | "name": "phar-io/manifest", 290 | "version": "2.0.3", 291 | "source": { 292 | "type": "git", 293 | "url": "https://github.com/phar-io/manifest.git", 294 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53" 295 | }, 296 | "dist": { 297 | "type": "zip", 298 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", 299 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53", 300 | "shasum": "" 301 | }, 302 | "require": { 303 | "ext-dom": "*", 304 | "ext-phar": "*", 305 | "ext-xmlwriter": "*", 306 | "phar-io/version": "^3.0.1", 307 | "php": "^7.2 || ^8.0" 308 | }, 309 | "type": "library", 310 | "extra": { 311 | "branch-alias": { 312 | "dev-master": "2.0.x-dev" 313 | } 314 | }, 315 | "autoload": { 316 | "classmap": [ 317 | "src/" 318 | ] 319 | }, 320 | "notification-url": "https://packagist.org/downloads/", 321 | "license": [ 322 | "BSD-3-Clause" 323 | ], 324 | "authors": [ 325 | { 326 | "name": "Arne Blankerts", 327 | "email": "arne@blankerts.de", 328 | "role": "Developer" 329 | }, 330 | { 331 | "name": "Sebastian Heuer", 332 | "email": "sebastian@phpeople.de", 333 | "role": "Developer" 334 | }, 335 | { 336 | "name": "Sebastian Bergmann", 337 | "email": "sebastian@phpunit.de", 338 | "role": "Developer" 339 | } 340 | ], 341 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 342 | "support": { 343 | "issues": "https://github.com/phar-io/manifest/issues", 344 | "source": "https://github.com/phar-io/manifest/tree/2.0.3" 345 | }, 346 | "time": "2021-07-20T11:28:43+00:00" 347 | }, 348 | { 349 | "name": "phar-io/version", 350 | "version": "3.2.1", 351 | "source": { 352 | "type": "git", 353 | "url": "https://github.com/phar-io/version.git", 354 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 355 | }, 356 | "dist": { 357 | "type": "zip", 358 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 359 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 360 | "shasum": "" 361 | }, 362 | "require": { 363 | "php": "^7.2 || ^8.0" 364 | }, 365 | "type": "library", 366 | "autoload": { 367 | "classmap": [ 368 | "src/" 369 | ] 370 | }, 371 | "notification-url": "https://packagist.org/downloads/", 372 | "license": [ 373 | "BSD-3-Clause" 374 | ], 375 | "authors": [ 376 | { 377 | "name": "Arne Blankerts", 378 | "email": "arne@blankerts.de", 379 | "role": "Developer" 380 | }, 381 | { 382 | "name": "Sebastian Heuer", 383 | "email": "sebastian@phpeople.de", 384 | "role": "Developer" 385 | }, 386 | { 387 | "name": "Sebastian Bergmann", 388 | "email": "sebastian@phpunit.de", 389 | "role": "Developer" 390 | } 391 | ], 392 | "description": "Library for handling version information and constraints", 393 | "support": { 394 | "issues": "https://github.com/phar-io/version/issues", 395 | "source": "https://github.com/phar-io/version/tree/3.2.1" 396 | }, 397 | "time": "2022-02-21T01:04:05+00:00" 398 | }, 399 | { 400 | "name": "phpunit/php-code-coverage", 401 | "version": "9.2.29", 402 | "source": { 403 | "type": "git", 404 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 405 | "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" 406 | }, 407 | "dist": { 408 | "type": "zip", 409 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", 410 | "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", 411 | "shasum": "" 412 | }, 413 | "require": { 414 | "ext-dom": "*", 415 | "ext-libxml": "*", 416 | "ext-xmlwriter": "*", 417 | "nikic/php-parser": "^4.15", 418 | "php": ">=7.3", 419 | "phpunit/php-file-iterator": "^3.0.3", 420 | "phpunit/php-text-template": "^2.0.2", 421 | "sebastian/code-unit-reverse-lookup": "^2.0.2", 422 | "sebastian/complexity": "^2.0", 423 | "sebastian/environment": "^5.1.2", 424 | "sebastian/lines-of-code": "^1.0.3", 425 | "sebastian/version": "^3.0.1", 426 | "theseer/tokenizer": "^1.2.0" 427 | }, 428 | "require-dev": { 429 | "phpunit/phpunit": "^9.3" 430 | }, 431 | "suggest": { 432 | "ext-pcov": "PHP extension that provides line coverage", 433 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 434 | }, 435 | "type": "library", 436 | "extra": { 437 | "branch-alias": { 438 | "dev-master": "9.2-dev" 439 | } 440 | }, 441 | "autoload": { 442 | "classmap": [ 443 | "src/" 444 | ] 445 | }, 446 | "notification-url": "https://packagist.org/downloads/", 447 | "license": [ 448 | "BSD-3-Clause" 449 | ], 450 | "authors": [ 451 | { 452 | "name": "Sebastian Bergmann", 453 | "email": "sebastian@phpunit.de", 454 | "role": "lead" 455 | } 456 | ], 457 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 458 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 459 | "keywords": [ 460 | "coverage", 461 | "testing", 462 | "xunit" 463 | ], 464 | "support": { 465 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 466 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 467 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" 468 | }, 469 | "funding": [ 470 | { 471 | "url": "https://github.com/sebastianbergmann", 472 | "type": "github" 473 | } 474 | ], 475 | "time": "2023-09-19T04:57:46+00:00" 476 | }, 477 | { 478 | "name": "phpunit/php-file-iterator", 479 | "version": "3.0.6", 480 | "source": { 481 | "type": "git", 482 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 483 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" 484 | }, 485 | "dist": { 486 | "type": "zip", 487 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 488 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 489 | "shasum": "" 490 | }, 491 | "require": { 492 | "php": ">=7.3" 493 | }, 494 | "require-dev": { 495 | "phpunit/phpunit": "^9.3" 496 | }, 497 | "type": "library", 498 | "extra": { 499 | "branch-alias": { 500 | "dev-master": "3.0-dev" 501 | } 502 | }, 503 | "autoload": { 504 | "classmap": [ 505 | "src/" 506 | ] 507 | }, 508 | "notification-url": "https://packagist.org/downloads/", 509 | "license": [ 510 | "BSD-3-Clause" 511 | ], 512 | "authors": [ 513 | { 514 | "name": "Sebastian Bergmann", 515 | "email": "sebastian@phpunit.de", 516 | "role": "lead" 517 | } 518 | ], 519 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 520 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 521 | "keywords": [ 522 | "filesystem", 523 | "iterator" 524 | ], 525 | "support": { 526 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 527 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" 528 | }, 529 | "funding": [ 530 | { 531 | "url": "https://github.com/sebastianbergmann", 532 | "type": "github" 533 | } 534 | ], 535 | "time": "2021-12-02T12:48:52+00:00" 536 | }, 537 | { 538 | "name": "phpunit/php-invoker", 539 | "version": "3.1.1", 540 | "source": { 541 | "type": "git", 542 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 543 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" 544 | }, 545 | "dist": { 546 | "type": "zip", 547 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 548 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 549 | "shasum": "" 550 | }, 551 | "require": { 552 | "php": ">=7.3" 553 | }, 554 | "require-dev": { 555 | "ext-pcntl": "*", 556 | "phpunit/phpunit": "^9.3" 557 | }, 558 | "suggest": { 559 | "ext-pcntl": "*" 560 | }, 561 | "type": "library", 562 | "extra": { 563 | "branch-alias": { 564 | "dev-master": "3.1-dev" 565 | } 566 | }, 567 | "autoload": { 568 | "classmap": [ 569 | "src/" 570 | ] 571 | }, 572 | "notification-url": "https://packagist.org/downloads/", 573 | "license": [ 574 | "BSD-3-Clause" 575 | ], 576 | "authors": [ 577 | { 578 | "name": "Sebastian Bergmann", 579 | "email": "sebastian@phpunit.de", 580 | "role": "lead" 581 | } 582 | ], 583 | "description": "Invoke callables with a timeout", 584 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 585 | "keywords": [ 586 | "process" 587 | ], 588 | "support": { 589 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 590 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" 591 | }, 592 | "funding": [ 593 | { 594 | "url": "https://github.com/sebastianbergmann", 595 | "type": "github" 596 | } 597 | ], 598 | "time": "2020-09-28T05:58:55+00:00" 599 | }, 600 | { 601 | "name": "phpunit/php-text-template", 602 | "version": "2.0.4", 603 | "source": { 604 | "type": "git", 605 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 606 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" 607 | }, 608 | "dist": { 609 | "type": "zip", 610 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 611 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 612 | "shasum": "" 613 | }, 614 | "require": { 615 | "php": ">=7.3" 616 | }, 617 | "require-dev": { 618 | "phpunit/phpunit": "^9.3" 619 | }, 620 | "type": "library", 621 | "extra": { 622 | "branch-alias": { 623 | "dev-master": "2.0-dev" 624 | } 625 | }, 626 | "autoload": { 627 | "classmap": [ 628 | "src/" 629 | ] 630 | }, 631 | "notification-url": "https://packagist.org/downloads/", 632 | "license": [ 633 | "BSD-3-Clause" 634 | ], 635 | "authors": [ 636 | { 637 | "name": "Sebastian Bergmann", 638 | "email": "sebastian@phpunit.de", 639 | "role": "lead" 640 | } 641 | ], 642 | "description": "Simple template engine.", 643 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 644 | "keywords": [ 645 | "template" 646 | ], 647 | "support": { 648 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 649 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" 650 | }, 651 | "funding": [ 652 | { 653 | "url": "https://github.com/sebastianbergmann", 654 | "type": "github" 655 | } 656 | ], 657 | "time": "2020-10-26T05:33:50+00:00" 658 | }, 659 | { 660 | "name": "phpunit/php-timer", 661 | "version": "5.0.3", 662 | "source": { 663 | "type": "git", 664 | "url": "https://github.com/sebastianbergmann/php-timer.git", 665 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" 666 | }, 667 | "dist": { 668 | "type": "zip", 669 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 670 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 671 | "shasum": "" 672 | }, 673 | "require": { 674 | "php": ">=7.3" 675 | }, 676 | "require-dev": { 677 | "phpunit/phpunit": "^9.3" 678 | }, 679 | "type": "library", 680 | "extra": { 681 | "branch-alias": { 682 | "dev-master": "5.0-dev" 683 | } 684 | }, 685 | "autoload": { 686 | "classmap": [ 687 | "src/" 688 | ] 689 | }, 690 | "notification-url": "https://packagist.org/downloads/", 691 | "license": [ 692 | "BSD-3-Clause" 693 | ], 694 | "authors": [ 695 | { 696 | "name": "Sebastian Bergmann", 697 | "email": "sebastian@phpunit.de", 698 | "role": "lead" 699 | } 700 | ], 701 | "description": "Utility class for timing", 702 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 703 | "keywords": [ 704 | "timer" 705 | ], 706 | "support": { 707 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 708 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" 709 | }, 710 | "funding": [ 711 | { 712 | "url": "https://github.com/sebastianbergmann", 713 | "type": "github" 714 | } 715 | ], 716 | "time": "2020-10-26T13:16:10+00:00" 717 | }, 718 | { 719 | "name": "phpunit/phpunit", 720 | "version": "9.6.13", 721 | "source": { 722 | "type": "git", 723 | "url": "https://github.com/sebastianbergmann/phpunit.git", 724 | "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" 725 | }, 726 | "dist": { 727 | "type": "zip", 728 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", 729 | "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", 730 | "shasum": "" 731 | }, 732 | "require": { 733 | "doctrine/instantiator": "^1.3.1 || ^2", 734 | "ext-dom": "*", 735 | "ext-json": "*", 736 | "ext-libxml": "*", 737 | "ext-mbstring": "*", 738 | "ext-xml": "*", 739 | "ext-xmlwriter": "*", 740 | "myclabs/deep-copy": "^1.10.1", 741 | "phar-io/manifest": "^2.0.3", 742 | "phar-io/version": "^3.0.2", 743 | "php": ">=7.3", 744 | "phpunit/php-code-coverage": "^9.2.28", 745 | "phpunit/php-file-iterator": "^3.0.5", 746 | "phpunit/php-invoker": "^3.1.1", 747 | "phpunit/php-text-template": "^2.0.3", 748 | "phpunit/php-timer": "^5.0.2", 749 | "sebastian/cli-parser": "^1.0.1", 750 | "sebastian/code-unit": "^1.0.6", 751 | "sebastian/comparator": "^4.0.8", 752 | "sebastian/diff": "^4.0.3", 753 | "sebastian/environment": "^5.1.3", 754 | "sebastian/exporter": "^4.0.5", 755 | "sebastian/global-state": "^5.0.1", 756 | "sebastian/object-enumerator": "^4.0.3", 757 | "sebastian/resource-operations": "^3.0.3", 758 | "sebastian/type": "^3.2", 759 | "sebastian/version": "^3.0.2" 760 | }, 761 | "suggest": { 762 | "ext-soap": "To be able to generate mocks based on WSDL files", 763 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 764 | }, 765 | "bin": [ 766 | "phpunit" 767 | ], 768 | "type": "library", 769 | "extra": { 770 | "branch-alias": { 771 | "dev-master": "9.6-dev" 772 | } 773 | }, 774 | "autoload": { 775 | "files": [ 776 | "src/Framework/Assert/Functions.php" 777 | ], 778 | "classmap": [ 779 | "src/" 780 | ] 781 | }, 782 | "notification-url": "https://packagist.org/downloads/", 783 | "license": [ 784 | "BSD-3-Clause" 785 | ], 786 | "authors": [ 787 | { 788 | "name": "Sebastian Bergmann", 789 | "email": "sebastian@phpunit.de", 790 | "role": "lead" 791 | } 792 | ], 793 | "description": "The PHP Unit Testing framework.", 794 | "homepage": "https://phpunit.de/", 795 | "keywords": [ 796 | "phpunit", 797 | "testing", 798 | "xunit" 799 | ], 800 | "support": { 801 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 802 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 803 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" 804 | }, 805 | "funding": [ 806 | { 807 | "url": "https://phpunit.de/sponsors.html", 808 | "type": "custom" 809 | }, 810 | { 811 | "url": "https://github.com/sebastianbergmann", 812 | "type": "github" 813 | }, 814 | { 815 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 816 | "type": "tidelift" 817 | } 818 | ], 819 | "time": "2023-09-19T05:39:22+00:00" 820 | }, 821 | { 822 | "name": "sebastian/cli-parser", 823 | "version": "1.0.1", 824 | "source": { 825 | "type": "git", 826 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 827 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" 828 | }, 829 | "dist": { 830 | "type": "zip", 831 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", 832 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", 833 | "shasum": "" 834 | }, 835 | "require": { 836 | "php": ">=7.3" 837 | }, 838 | "require-dev": { 839 | "phpunit/phpunit": "^9.3" 840 | }, 841 | "type": "library", 842 | "extra": { 843 | "branch-alias": { 844 | "dev-master": "1.0-dev" 845 | } 846 | }, 847 | "autoload": { 848 | "classmap": [ 849 | "src/" 850 | ] 851 | }, 852 | "notification-url": "https://packagist.org/downloads/", 853 | "license": [ 854 | "BSD-3-Clause" 855 | ], 856 | "authors": [ 857 | { 858 | "name": "Sebastian Bergmann", 859 | "email": "sebastian@phpunit.de", 860 | "role": "lead" 861 | } 862 | ], 863 | "description": "Library for parsing CLI options", 864 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 865 | "support": { 866 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 867 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" 868 | }, 869 | "funding": [ 870 | { 871 | "url": "https://github.com/sebastianbergmann", 872 | "type": "github" 873 | } 874 | ], 875 | "time": "2020-09-28T06:08:49+00:00" 876 | }, 877 | { 878 | "name": "sebastian/code-unit", 879 | "version": "1.0.8", 880 | "source": { 881 | "type": "git", 882 | "url": "https://github.com/sebastianbergmann/code-unit.git", 883 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" 884 | }, 885 | "dist": { 886 | "type": "zip", 887 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", 888 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", 889 | "shasum": "" 890 | }, 891 | "require": { 892 | "php": ">=7.3" 893 | }, 894 | "require-dev": { 895 | "phpunit/phpunit": "^9.3" 896 | }, 897 | "type": "library", 898 | "extra": { 899 | "branch-alias": { 900 | "dev-master": "1.0-dev" 901 | } 902 | }, 903 | "autoload": { 904 | "classmap": [ 905 | "src/" 906 | ] 907 | }, 908 | "notification-url": "https://packagist.org/downloads/", 909 | "license": [ 910 | "BSD-3-Clause" 911 | ], 912 | "authors": [ 913 | { 914 | "name": "Sebastian Bergmann", 915 | "email": "sebastian@phpunit.de", 916 | "role": "lead" 917 | } 918 | ], 919 | "description": "Collection of value objects that represent the PHP code units", 920 | "homepage": "https://github.com/sebastianbergmann/code-unit", 921 | "support": { 922 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 923 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" 924 | }, 925 | "funding": [ 926 | { 927 | "url": "https://github.com/sebastianbergmann", 928 | "type": "github" 929 | } 930 | ], 931 | "time": "2020-10-26T13:08:54+00:00" 932 | }, 933 | { 934 | "name": "sebastian/code-unit-reverse-lookup", 935 | "version": "2.0.3", 936 | "source": { 937 | "type": "git", 938 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 939 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" 940 | }, 941 | "dist": { 942 | "type": "zip", 943 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 944 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 945 | "shasum": "" 946 | }, 947 | "require": { 948 | "php": ">=7.3" 949 | }, 950 | "require-dev": { 951 | "phpunit/phpunit": "^9.3" 952 | }, 953 | "type": "library", 954 | "extra": { 955 | "branch-alias": { 956 | "dev-master": "2.0-dev" 957 | } 958 | }, 959 | "autoload": { 960 | "classmap": [ 961 | "src/" 962 | ] 963 | }, 964 | "notification-url": "https://packagist.org/downloads/", 965 | "license": [ 966 | "BSD-3-Clause" 967 | ], 968 | "authors": [ 969 | { 970 | "name": "Sebastian Bergmann", 971 | "email": "sebastian@phpunit.de" 972 | } 973 | ], 974 | "description": "Looks up which function or method a line of code belongs to", 975 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 976 | "support": { 977 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 978 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" 979 | }, 980 | "funding": [ 981 | { 982 | "url": "https://github.com/sebastianbergmann", 983 | "type": "github" 984 | } 985 | ], 986 | "time": "2020-09-28T05:30:19+00:00" 987 | }, 988 | { 989 | "name": "sebastian/comparator", 990 | "version": "4.0.8", 991 | "source": { 992 | "type": "git", 993 | "url": "https://github.com/sebastianbergmann/comparator.git", 994 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a" 995 | }, 996 | "dist": { 997 | "type": "zip", 998 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", 999 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a", 1000 | "shasum": "" 1001 | }, 1002 | "require": { 1003 | "php": ">=7.3", 1004 | "sebastian/diff": "^4.0", 1005 | "sebastian/exporter": "^4.0" 1006 | }, 1007 | "require-dev": { 1008 | "phpunit/phpunit": "^9.3" 1009 | }, 1010 | "type": "library", 1011 | "extra": { 1012 | "branch-alias": { 1013 | "dev-master": "4.0-dev" 1014 | } 1015 | }, 1016 | "autoload": { 1017 | "classmap": [ 1018 | "src/" 1019 | ] 1020 | }, 1021 | "notification-url": "https://packagist.org/downloads/", 1022 | "license": [ 1023 | "BSD-3-Clause" 1024 | ], 1025 | "authors": [ 1026 | { 1027 | "name": "Sebastian Bergmann", 1028 | "email": "sebastian@phpunit.de" 1029 | }, 1030 | { 1031 | "name": "Jeff Welch", 1032 | "email": "whatthejeff@gmail.com" 1033 | }, 1034 | { 1035 | "name": "Volker Dusch", 1036 | "email": "github@wallbash.com" 1037 | }, 1038 | { 1039 | "name": "Bernhard Schussek", 1040 | "email": "bschussek@2bepublished.at" 1041 | } 1042 | ], 1043 | "description": "Provides the functionality to compare PHP values for equality", 1044 | "homepage": "https://github.com/sebastianbergmann/comparator", 1045 | "keywords": [ 1046 | "comparator", 1047 | "compare", 1048 | "equality" 1049 | ], 1050 | "support": { 1051 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 1052 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" 1053 | }, 1054 | "funding": [ 1055 | { 1056 | "url": "https://github.com/sebastianbergmann", 1057 | "type": "github" 1058 | } 1059 | ], 1060 | "time": "2022-09-14T12:41:17+00:00" 1061 | }, 1062 | { 1063 | "name": "sebastian/complexity", 1064 | "version": "2.0.2", 1065 | "source": { 1066 | "type": "git", 1067 | "url": "https://github.com/sebastianbergmann/complexity.git", 1068 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" 1069 | }, 1070 | "dist": { 1071 | "type": "zip", 1072 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", 1073 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", 1074 | "shasum": "" 1075 | }, 1076 | "require": { 1077 | "nikic/php-parser": "^4.7", 1078 | "php": ">=7.3" 1079 | }, 1080 | "require-dev": { 1081 | "phpunit/phpunit": "^9.3" 1082 | }, 1083 | "type": "library", 1084 | "extra": { 1085 | "branch-alias": { 1086 | "dev-master": "2.0-dev" 1087 | } 1088 | }, 1089 | "autoload": { 1090 | "classmap": [ 1091 | "src/" 1092 | ] 1093 | }, 1094 | "notification-url": "https://packagist.org/downloads/", 1095 | "license": [ 1096 | "BSD-3-Clause" 1097 | ], 1098 | "authors": [ 1099 | { 1100 | "name": "Sebastian Bergmann", 1101 | "email": "sebastian@phpunit.de", 1102 | "role": "lead" 1103 | } 1104 | ], 1105 | "description": "Library for calculating the complexity of PHP code units", 1106 | "homepage": "https://github.com/sebastianbergmann/complexity", 1107 | "support": { 1108 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 1109 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" 1110 | }, 1111 | "funding": [ 1112 | { 1113 | "url": "https://github.com/sebastianbergmann", 1114 | "type": "github" 1115 | } 1116 | ], 1117 | "time": "2020-10-26T15:52:27+00:00" 1118 | }, 1119 | { 1120 | "name": "sebastian/diff", 1121 | "version": "4.0.5", 1122 | "source": { 1123 | "type": "git", 1124 | "url": "https://github.com/sebastianbergmann/diff.git", 1125 | "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" 1126 | }, 1127 | "dist": { 1128 | "type": "zip", 1129 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", 1130 | "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", 1131 | "shasum": "" 1132 | }, 1133 | "require": { 1134 | "php": ">=7.3" 1135 | }, 1136 | "require-dev": { 1137 | "phpunit/phpunit": "^9.3", 1138 | "symfony/process": "^4.2 || ^5" 1139 | }, 1140 | "type": "library", 1141 | "extra": { 1142 | "branch-alias": { 1143 | "dev-master": "4.0-dev" 1144 | } 1145 | }, 1146 | "autoload": { 1147 | "classmap": [ 1148 | "src/" 1149 | ] 1150 | }, 1151 | "notification-url": "https://packagist.org/downloads/", 1152 | "license": [ 1153 | "BSD-3-Clause" 1154 | ], 1155 | "authors": [ 1156 | { 1157 | "name": "Sebastian Bergmann", 1158 | "email": "sebastian@phpunit.de" 1159 | }, 1160 | { 1161 | "name": "Kore Nordmann", 1162 | "email": "mail@kore-nordmann.de" 1163 | } 1164 | ], 1165 | "description": "Diff implementation", 1166 | "homepage": "https://github.com/sebastianbergmann/diff", 1167 | "keywords": [ 1168 | "diff", 1169 | "udiff", 1170 | "unidiff", 1171 | "unified diff" 1172 | ], 1173 | "support": { 1174 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1175 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" 1176 | }, 1177 | "funding": [ 1178 | { 1179 | "url": "https://github.com/sebastianbergmann", 1180 | "type": "github" 1181 | } 1182 | ], 1183 | "time": "2023-05-07T05:35:17+00:00" 1184 | }, 1185 | { 1186 | "name": "sebastian/environment", 1187 | "version": "5.1.5", 1188 | "source": { 1189 | "type": "git", 1190 | "url": "https://github.com/sebastianbergmann/environment.git", 1191 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" 1192 | }, 1193 | "dist": { 1194 | "type": "zip", 1195 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1196 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1197 | "shasum": "" 1198 | }, 1199 | "require": { 1200 | "php": ">=7.3" 1201 | }, 1202 | "require-dev": { 1203 | "phpunit/phpunit": "^9.3" 1204 | }, 1205 | "suggest": { 1206 | "ext-posix": "*" 1207 | }, 1208 | "type": "library", 1209 | "extra": { 1210 | "branch-alias": { 1211 | "dev-master": "5.1-dev" 1212 | } 1213 | }, 1214 | "autoload": { 1215 | "classmap": [ 1216 | "src/" 1217 | ] 1218 | }, 1219 | "notification-url": "https://packagist.org/downloads/", 1220 | "license": [ 1221 | "BSD-3-Clause" 1222 | ], 1223 | "authors": [ 1224 | { 1225 | "name": "Sebastian Bergmann", 1226 | "email": "sebastian@phpunit.de" 1227 | } 1228 | ], 1229 | "description": "Provides functionality to handle HHVM/PHP environments", 1230 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1231 | "keywords": [ 1232 | "Xdebug", 1233 | "environment", 1234 | "hhvm" 1235 | ], 1236 | "support": { 1237 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1238 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" 1239 | }, 1240 | "funding": [ 1241 | { 1242 | "url": "https://github.com/sebastianbergmann", 1243 | "type": "github" 1244 | } 1245 | ], 1246 | "time": "2023-02-03T06:03:51+00:00" 1247 | }, 1248 | { 1249 | "name": "sebastian/exporter", 1250 | "version": "4.0.5", 1251 | "source": { 1252 | "type": "git", 1253 | "url": "https://github.com/sebastianbergmann/exporter.git", 1254 | "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" 1255 | }, 1256 | "dist": { 1257 | "type": "zip", 1258 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", 1259 | "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", 1260 | "shasum": "" 1261 | }, 1262 | "require": { 1263 | "php": ">=7.3", 1264 | "sebastian/recursion-context": "^4.0" 1265 | }, 1266 | "require-dev": { 1267 | "ext-mbstring": "*", 1268 | "phpunit/phpunit": "^9.3" 1269 | }, 1270 | "type": "library", 1271 | "extra": { 1272 | "branch-alias": { 1273 | "dev-master": "4.0-dev" 1274 | } 1275 | }, 1276 | "autoload": { 1277 | "classmap": [ 1278 | "src/" 1279 | ] 1280 | }, 1281 | "notification-url": "https://packagist.org/downloads/", 1282 | "license": [ 1283 | "BSD-3-Clause" 1284 | ], 1285 | "authors": [ 1286 | { 1287 | "name": "Sebastian Bergmann", 1288 | "email": "sebastian@phpunit.de" 1289 | }, 1290 | { 1291 | "name": "Jeff Welch", 1292 | "email": "whatthejeff@gmail.com" 1293 | }, 1294 | { 1295 | "name": "Volker Dusch", 1296 | "email": "github@wallbash.com" 1297 | }, 1298 | { 1299 | "name": "Adam Harvey", 1300 | "email": "aharvey@php.net" 1301 | }, 1302 | { 1303 | "name": "Bernhard Schussek", 1304 | "email": "bschussek@gmail.com" 1305 | } 1306 | ], 1307 | "description": "Provides the functionality to export PHP variables for visualization", 1308 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1309 | "keywords": [ 1310 | "export", 1311 | "exporter" 1312 | ], 1313 | "support": { 1314 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1315 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" 1316 | }, 1317 | "funding": [ 1318 | { 1319 | "url": "https://github.com/sebastianbergmann", 1320 | "type": "github" 1321 | } 1322 | ], 1323 | "time": "2022-09-14T06:03:37+00:00" 1324 | }, 1325 | { 1326 | "name": "sebastian/global-state", 1327 | "version": "5.0.6", 1328 | "source": { 1329 | "type": "git", 1330 | "url": "https://github.com/sebastianbergmann/global-state.git", 1331 | "reference": "bde739e7565280bda77be70044ac1047bc007e34" 1332 | }, 1333 | "dist": { 1334 | "type": "zip", 1335 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", 1336 | "reference": "bde739e7565280bda77be70044ac1047bc007e34", 1337 | "shasum": "" 1338 | }, 1339 | "require": { 1340 | "php": ">=7.3", 1341 | "sebastian/object-reflector": "^2.0", 1342 | "sebastian/recursion-context": "^4.0" 1343 | }, 1344 | "require-dev": { 1345 | "ext-dom": "*", 1346 | "phpunit/phpunit": "^9.3" 1347 | }, 1348 | "suggest": { 1349 | "ext-uopz": "*" 1350 | }, 1351 | "type": "library", 1352 | "extra": { 1353 | "branch-alias": { 1354 | "dev-master": "5.0-dev" 1355 | } 1356 | }, 1357 | "autoload": { 1358 | "classmap": [ 1359 | "src/" 1360 | ] 1361 | }, 1362 | "notification-url": "https://packagist.org/downloads/", 1363 | "license": [ 1364 | "BSD-3-Clause" 1365 | ], 1366 | "authors": [ 1367 | { 1368 | "name": "Sebastian Bergmann", 1369 | "email": "sebastian@phpunit.de" 1370 | } 1371 | ], 1372 | "description": "Snapshotting of global state", 1373 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1374 | "keywords": [ 1375 | "global state" 1376 | ], 1377 | "support": { 1378 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1379 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" 1380 | }, 1381 | "funding": [ 1382 | { 1383 | "url": "https://github.com/sebastianbergmann", 1384 | "type": "github" 1385 | } 1386 | ], 1387 | "time": "2023-08-02T09:26:13+00:00" 1388 | }, 1389 | { 1390 | "name": "sebastian/lines-of-code", 1391 | "version": "1.0.3", 1392 | "source": { 1393 | "type": "git", 1394 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1395 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" 1396 | }, 1397 | "dist": { 1398 | "type": "zip", 1399 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1400 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1401 | "shasum": "" 1402 | }, 1403 | "require": { 1404 | "nikic/php-parser": "^4.6", 1405 | "php": ">=7.3" 1406 | }, 1407 | "require-dev": { 1408 | "phpunit/phpunit": "^9.3" 1409 | }, 1410 | "type": "library", 1411 | "extra": { 1412 | "branch-alias": { 1413 | "dev-master": "1.0-dev" 1414 | } 1415 | }, 1416 | "autoload": { 1417 | "classmap": [ 1418 | "src/" 1419 | ] 1420 | }, 1421 | "notification-url": "https://packagist.org/downloads/", 1422 | "license": [ 1423 | "BSD-3-Clause" 1424 | ], 1425 | "authors": [ 1426 | { 1427 | "name": "Sebastian Bergmann", 1428 | "email": "sebastian@phpunit.de", 1429 | "role": "lead" 1430 | } 1431 | ], 1432 | "description": "Library for counting the lines of code in PHP source code", 1433 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1434 | "support": { 1435 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1436 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" 1437 | }, 1438 | "funding": [ 1439 | { 1440 | "url": "https://github.com/sebastianbergmann", 1441 | "type": "github" 1442 | } 1443 | ], 1444 | "time": "2020-11-28T06:42:11+00:00" 1445 | }, 1446 | { 1447 | "name": "sebastian/object-enumerator", 1448 | "version": "4.0.4", 1449 | "source": { 1450 | "type": "git", 1451 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1452 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" 1453 | }, 1454 | "dist": { 1455 | "type": "zip", 1456 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", 1457 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", 1458 | "shasum": "" 1459 | }, 1460 | "require": { 1461 | "php": ">=7.3", 1462 | "sebastian/object-reflector": "^2.0", 1463 | "sebastian/recursion-context": "^4.0" 1464 | }, 1465 | "require-dev": { 1466 | "phpunit/phpunit": "^9.3" 1467 | }, 1468 | "type": "library", 1469 | "extra": { 1470 | "branch-alias": { 1471 | "dev-master": "4.0-dev" 1472 | } 1473 | }, 1474 | "autoload": { 1475 | "classmap": [ 1476 | "src/" 1477 | ] 1478 | }, 1479 | "notification-url": "https://packagist.org/downloads/", 1480 | "license": [ 1481 | "BSD-3-Clause" 1482 | ], 1483 | "authors": [ 1484 | { 1485 | "name": "Sebastian Bergmann", 1486 | "email": "sebastian@phpunit.de" 1487 | } 1488 | ], 1489 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1490 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1491 | "support": { 1492 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1493 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" 1494 | }, 1495 | "funding": [ 1496 | { 1497 | "url": "https://github.com/sebastianbergmann", 1498 | "type": "github" 1499 | } 1500 | ], 1501 | "time": "2020-10-26T13:12:34+00:00" 1502 | }, 1503 | { 1504 | "name": "sebastian/object-reflector", 1505 | "version": "2.0.4", 1506 | "source": { 1507 | "type": "git", 1508 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1509 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" 1510 | }, 1511 | "dist": { 1512 | "type": "zip", 1513 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1514 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1515 | "shasum": "" 1516 | }, 1517 | "require": { 1518 | "php": ">=7.3" 1519 | }, 1520 | "require-dev": { 1521 | "phpunit/phpunit": "^9.3" 1522 | }, 1523 | "type": "library", 1524 | "extra": { 1525 | "branch-alias": { 1526 | "dev-master": "2.0-dev" 1527 | } 1528 | }, 1529 | "autoload": { 1530 | "classmap": [ 1531 | "src/" 1532 | ] 1533 | }, 1534 | "notification-url": "https://packagist.org/downloads/", 1535 | "license": [ 1536 | "BSD-3-Clause" 1537 | ], 1538 | "authors": [ 1539 | { 1540 | "name": "Sebastian Bergmann", 1541 | "email": "sebastian@phpunit.de" 1542 | } 1543 | ], 1544 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1545 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1546 | "support": { 1547 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1548 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" 1549 | }, 1550 | "funding": [ 1551 | { 1552 | "url": "https://github.com/sebastianbergmann", 1553 | "type": "github" 1554 | } 1555 | ], 1556 | "time": "2020-10-26T13:14:26+00:00" 1557 | }, 1558 | { 1559 | "name": "sebastian/recursion-context", 1560 | "version": "4.0.5", 1561 | "source": { 1562 | "type": "git", 1563 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1564 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" 1565 | }, 1566 | "dist": { 1567 | "type": "zip", 1568 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1569 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1570 | "shasum": "" 1571 | }, 1572 | "require": { 1573 | "php": ">=7.3" 1574 | }, 1575 | "require-dev": { 1576 | "phpunit/phpunit": "^9.3" 1577 | }, 1578 | "type": "library", 1579 | "extra": { 1580 | "branch-alias": { 1581 | "dev-master": "4.0-dev" 1582 | } 1583 | }, 1584 | "autoload": { 1585 | "classmap": [ 1586 | "src/" 1587 | ] 1588 | }, 1589 | "notification-url": "https://packagist.org/downloads/", 1590 | "license": [ 1591 | "BSD-3-Clause" 1592 | ], 1593 | "authors": [ 1594 | { 1595 | "name": "Sebastian Bergmann", 1596 | "email": "sebastian@phpunit.de" 1597 | }, 1598 | { 1599 | "name": "Jeff Welch", 1600 | "email": "whatthejeff@gmail.com" 1601 | }, 1602 | { 1603 | "name": "Adam Harvey", 1604 | "email": "aharvey@php.net" 1605 | } 1606 | ], 1607 | "description": "Provides functionality to recursively process PHP variables", 1608 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1609 | "support": { 1610 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1611 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" 1612 | }, 1613 | "funding": [ 1614 | { 1615 | "url": "https://github.com/sebastianbergmann", 1616 | "type": "github" 1617 | } 1618 | ], 1619 | "time": "2023-02-03T06:07:39+00:00" 1620 | }, 1621 | { 1622 | "name": "sebastian/resource-operations", 1623 | "version": "3.0.3", 1624 | "source": { 1625 | "type": "git", 1626 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1627 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" 1628 | }, 1629 | "dist": { 1630 | "type": "zip", 1631 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1632 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1633 | "shasum": "" 1634 | }, 1635 | "require": { 1636 | "php": ">=7.3" 1637 | }, 1638 | "require-dev": { 1639 | "phpunit/phpunit": "^9.0" 1640 | }, 1641 | "type": "library", 1642 | "extra": { 1643 | "branch-alias": { 1644 | "dev-master": "3.0-dev" 1645 | } 1646 | }, 1647 | "autoload": { 1648 | "classmap": [ 1649 | "src/" 1650 | ] 1651 | }, 1652 | "notification-url": "https://packagist.org/downloads/", 1653 | "license": [ 1654 | "BSD-3-Clause" 1655 | ], 1656 | "authors": [ 1657 | { 1658 | "name": "Sebastian Bergmann", 1659 | "email": "sebastian@phpunit.de" 1660 | } 1661 | ], 1662 | "description": "Provides a list of PHP built-in functions that operate on resources", 1663 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1664 | "support": { 1665 | "issues": "https://github.com/sebastianbergmann/resource-operations/issues", 1666 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" 1667 | }, 1668 | "funding": [ 1669 | { 1670 | "url": "https://github.com/sebastianbergmann", 1671 | "type": "github" 1672 | } 1673 | ], 1674 | "time": "2020-09-28T06:45:17+00:00" 1675 | }, 1676 | { 1677 | "name": "sebastian/type", 1678 | "version": "3.2.1", 1679 | "source": { 1680 | "type": "git", 1681 | "url": "https://github.com/sebastianbergmann/type.git", 1682 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" 1683 | }, 1684 | "dist": { 1685 | "type": "zip", 1686 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 1687 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 1688 | "shasum": "" 1689 | }, 1690 | "require": { 1691 | "php": ">=7.3" 1692 | }, 1693 | "require-dev": { 1694 | "phpunit/phpunit": "^9.5" 1695 | }, 1696 | "type": "library", 1697 | "extra": { 1698 | "branch-alias": { 1699 | "dev-master": "3.2-dev" 1700 | } 1701 | }, 1702 | "autoload": { 1703 | "classmap": [ 1704 | "src/" 1705 | ] 1706 | }, 1707 | "notification-url": "https://packagist.org/downloads/", 1708 | "license": [ 1709 | "BSD-3-Clause" 1710 | ], 1711 | "authors": [ 1712 | { 1713 | "name": "Sebastian Bergmann", 1714 | "email": "sebastian@phpunit.de", 1715 | "role": "lead" 1716 | } 1717 | ], 1718 | "description": "Collection of value objects that represent the types of the PHP type system", 1719 | "homepage": "https://github.com/sebastianbergmann/type", 1720 | "support": { 1721 | "issues": "https://github.com/sebastianbergmann/type/issues", 1722 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" 1723 | }, 1724 | "funding": [ 1725 | { 1726 | "url": "https://github.com/sebastianbergmann", 1727 | "type": "github" 1728 | } 1729 | ], 1730 | "time": "2023-02-03T06:13:03+00:00" 1731 | }, 1732 | { 1733 | "name": "sebastian/version", 1734 | "version": "3.0.2", 1735 | "source": { 1736 | "type": "git", 1737 | "url": "https://github.com/sebastianbergmann/version.git", 1738 | "reference": "c6c1022351a901512170118436c764e473f6de8c" 1739 | }, 1740 | "dist": { 1741 | "type": "zip", 1742 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", 1743 | "reference": "c6c1022351a901512170118436c764e473f6de8c", 1744 | "shasum": "" 1745 | }, 1746 | "require": { 1747 | "php": ">=7.3" 1748 | }, 1749 | "type": "library", 1750 | "extra": { 1751 | "branch-alias": { 1752 | "dev-master": "3.0-dev" 1753 | } 1754 | }, 1755 | "autoload": { 1756 | "classmap": [ 1757 | "src/" 1758 | ] 1759 | }, 1760 | "notification-url": "https://packagist.org/downloads/", 1761 | "license": [ 1762 | "BSD-3-Clause" 1763 | ], 1764 | "authors": [ 1765 | { 1766 | "name": "Sebastian Bergmann", 1767 | "email": "sebastian@phpunit.de", 1768 | "role": "lead" 1769 | } 1770 | ], 1771 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1772 | "homepage": "https://github.com/sebastianbergmann/version", 1773 | "support": { 1774 | "issues": "https://github.com/sebastianbergmann/version/issues", 1775 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" 1776 | }, 1777 | "funding": [ 1778 | { 1779 | "url": "https://github.com/sebastianbergmann", 1780 | "type": "github" 1781 | } 1782 | ], 1783 | "time": "2020-09-28T06:39:44+00:00" 1784 | }, 1785 | { 1786 | "name": "theseer/tokenizer", 1787 | "version": "1.2.1", 1788 | "source": { 1789 | "type": "git", 1790 | "url": "https://github.com/theseer/tokenizer.git", 1791 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" 1792 | }, 1793 | "dist": { 1794 | "type": "zip", 1795 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", 1796 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", 1797 | "shasum": "" 1798 | }, 1799 | "require": { 1800 | "ext-dom": "*", 1801 | "ext-tokenizer": "*", 1802 | "ext-xmlwriter": "*", 1803 | "php": "^7.2 || ^8.0" 1804 | }, 1805 | "type": "library", 1806 | "autoload": { 1807 | "classmap": [ 1808 | "src/" 1809 | ] 1810 | }, 1811 | "notification-url": "https://packagist.org/downloads/", 1812 | "license": [ 1813 | "BSD-3-Clause" 1814 | ], 1815 | "authors": [ 1816 | { 1817 | "name": "Arne Blankerts", 1818 | "email": "arne@blankerts.de", 1819 | "role": "Developer" 1820 | } 1821 | ], 1822 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1823 | "support": { 1824 | "issues": "https://github.com/theseer/tokenizer/issues", 1825 | "source": "https://github.com/theseer/tokenizer/tree/1.2.1" 1826 | }, 1827 | "funding": [ 1828 | { 1829 | "url": "https://github.com/theseer", 1830 | "type": "github" 1831 | } 1832 | ], 1833 | "time": "2021-07-28T10:34:58+00:00" 1834 | } 1835 | ], 1836 | "aliases": [], 1837 | "minimum-stability": "stable", 1838 | "stability-flags": [], 1839 | "prefer-stable": false, 1840 | "prefer-lowest": false, 1841 | "platform": [], 1842 | "platform-dev": [], 1843 | "plugin-api-version": "2.3.0" 1844 | } 1845 | -------------------------------------------------------------------------------- /config-default.ini: -------------------------------------------------------------------------------- 1 | db-user = 2 | db-password = 3 | 4 | db-meta-host = meta.web.db.svc.wikimedia.cloud 5 | db-meta-name = meta_p 6 | db-meta-port = 3306 7 | 8 | db-host = PROJECT.web.db.svc.wikimedia.cloud 9 | db-name = PROJECT_p 10 | db-port = 3306 11 | 12 | redis-server = localhost 13 | redis-port = 6379 14 | redis-auth = 15 | redis-prefix = 16 | 17 | test-db-user = root 18 | test-db-password = 19 | test-db-host = localhost 20 | test-db-port = 3306 21 | 22 | default-project = en.wikipedia.org 23 | useragent = 24 | -------------------------------------------------------------------------------- /config-example.ini: -------------------------------------------------------------------------------- 1 | ; Username for the database, user from replica.my.cnf 2 | ; db-user = 3 | ; Password for the database, password from replica.my.cnf 4 | ; db-password = 5 | 6 | ; Host for the meta databse 7 | ; db-meta-host = 8 | ; Name of the meta databse 9 | ; db-meta-name = 10 | ; Port for the meta database 11 | ; db-meta-port = 12 | 13 | ; Host format for the databases, PROJECT is replaced with the project name 14 | ; db-host = 15 | ; Name format of the databases, PROJECT is replaced with the project name 16 | ; db-name = 17 | ; Port for the databases 18 | ; db-port = 19 | 20 | ; Host for the redis server 21 | ; redis-server = 22 | ; Port for the redis server 23 | ; redis-port = 24 | ; Auth for the redis server 25 | ; redis-auth = 26 | ; Prefix for entries in the redis table 27 | ; redis-prefix = 28 | 29 | ; Username for the test database 30 | ; test-db-user = 31 | ; Password for the test database 32 | ; test-db-password = 33 | ; Host for the test database 34 | ; test-db-host = 35 | ; Port for the test database 36 | ; test-db-port = 37 | 38 | ; The default project to use 39 | ; default-project = 40 | ; User agent to use when making API request to the wiki 41 | ; Something like "TOOL_NAME (CONTACT_INFO)" 42 | ; useragent = 43 | -------------------------------------------------------------------------------- /includes/APIHelpExamples.php: -------------------------------------------------------------------------------- 1 | examples = $examples; 10 | $this->prefix = "//{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; 11 | } 12 | 13 | public function getHtml() { 14 | $out = ''; 15 | 16 | foreach ($this->examples as $example) { 17 | $url = $this->prefix . '?' . $example; 18 | $out .= "
  • $url
  • "; 19 | } 20 | 21 | return "
      $out
    "; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /includes/APIHelpObject.php: -------------------------------------------------------------------------------- 1 | keys = $keys; 9 | } 10 | 11 | public function getHtml() { 12 | $out = ''; 13 | 14 | foreach ($this->keys as list($key, $type, $status, $desc)) { 15 | $out .= "
  • $key - $status $type - $desc
  • "; 16 | } 17 | 18 | return "
      $out
    "; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /includes/Config.php: -------------------------------------------------------------------------------- 1 | fromNamespaces = $fromNamespaces; 20 | $this->db = $db; 21 | $this->title = $title; 22 | } 23 | 24 | private function createCond( 25 | string $prefix, 26 | string $titleSQL, 27 | string $namespaceSQL, 28 | int $flags, 29 | $joins = [], 30 | $wheres = [] 31 | ) { 32 | $hasFromNS = ~$flags & CountQuery::NO_FROM_NS; 33 | $usesLinkTarget = ~$flags & CountQuery::NO_LINK_TARGET; 34 | $hasNS = $usesLinkTarget || (~$flags & CountQuery::SINGLE_NS); 35 | 36 | if ($this->fromNamespaces !== '' && $hasFromNS) { 37 | array_push($wheres, "{$prefix}_from_namespace IN ({$this->fromNamespaces})"); 38 | } 39 | 40 | if ($this->fromNamespaces !== '' && !$hasFromNS) { 41 | array_push( 42 | $joins, 43 | <<fromNamespaces}) 47 | SQL 48 | ); 49 | } 50 | 51 | $linkInfoPrefix = $usesLinkTarget ? 'lt' : $prefix; 52 | $titleColumn = $linkInfoPrefix . '_' . ($hasNS ? 'title' : 'to'); 53 | 54 | array_push($wheres, "$titleColumn = $titleSQL"); 55 | 56 | if ($hasNS) { 57 | array_push($wheres, "{$linkInfoPrefix}_namespace = $namespaceSQL"); 58 | } 59 | 60 | if ($usesLinkTarget) { 61 | array_push($joins, "JOIN linktarget ON {$prefix}_target_id = lt_id"); 62 | } 63 | 64 | return implode(' ', $joins) . " WHERE " . implode(' AND ', $wheres); 65 | } 66 | 67 | private function createDirectCond(string $prefix, int $flags) { 68 | return $this->createCond( 69 | $prefix, 70 | $this->db->quote($this->title->getDBKey()), 71 | $this->title->getNamespaceId(), 72 | $flags 73 | ); 74 | } 75 | 76 | private function createIndirectCond(string $table, string $prefix, int $flags) { 77 | $joins = [ 78 | 'JOIN page AS target ON target.page_id = rd_from', 79 | "JOIN $table" 80 | ]; 81 | 82 | $wheres = [ 83 | "rd_title = {$this->db->quote($this->title->getDBKey())}", 84 | "rd_namespace = {$this->title->getNamespaceId()}", 85 | "(rd_interwiki IS NULL OR rd_interwiki = {$this->db->quote('')})" 86 | ]; 87 | 88 | return $this->createCond( 89 | $prefix, 90 | 'target.page_title', 91 | 'target.page_namespace', 92 | $flags, 93 | $joins, 94 | $wheres 95 | ); 96 | } 97 | 98 | private function createQuery(string $table, string $prefix, CountQueryMode $mode, int $flags) { 99 | return match ($mode) { 100 | CountQueryMode::Redirect => <<createDirectCond($prefix, $flags)} 103 | AND ({$prefix}_interwiki is NULL or {$prefix}_interwiki = {$this->db->quote('')}) 104 | SQL, 105 | // Transclusions of a redirect that follow the redirect are also added as a transclusion of the redirect target. 106 | // There is no way to differentiate from a page with a indirect link and a page with a indirect and a direct link 107 | // in this case, only the indirect link is recorded. Pages can also transclude a page with a redirect without 108 | // following the redirect, so a valid indirect link must have an associated direct link. 109 | CountQueryMode::Transclusion => <<createIndirectCond($table, $prefix, $flags)} 119 | ) AS temp ON {$prefix}_from = indirect_link 120 | {$this->createDirectCond($prefix, $flags)} 121 | SQL, 122 | CountQueryMode::Link => <<createDirectCond($prefix, $flags)} 131 | UNION ALL 132 | SELECT DISTINCT NULL AS direct_link, {$prefix}_from AS indirect_link 133 | FROM redirect 134 | {$this->createIndirectCond($table, $prefix, $flags)} 135 | ) AS temp 136 | SQL 137 | }; 138 | } 139 | 140 | public function runQuery(string $table, string $prefix, CountQueryMode $mode, $flags = 0) { 141 | $query = $this->createQuery($table, $prefix, $mode, $flags); 142 | $res = $this->db->query($query)->fetch(); 143 | 144 | return $mode == CountQueryMode::Redirect ? (int) $res[0] : [ 145 | 'all' => (int) $res[0], 146 | 'direct' => (int) $res[1], 147 | 'indirect' => (int) $res[2] 148 | ]; 149 | } 150 | } -------------------------------------------------------------------------------- /includes/DatabaseFactory.php: -------------------------------------------------------------------------------- 1 | rel = $rel; 8 | } 9 | 10 | public function getHtml() { 11 | $gitHubLink = (new OOUI\Tag('a'))->setAttributes([ 12 | 'href' => 'https://github.com/BrandonXLF/linkcount' 13 | ])->appendContent('GitHub'); 14 | 15 | $revLink = (new OOUI\Tag('a'))->setAttributes([ 16 | 'href' => "https://github.com/BrandonXLF/linkcount/tree/" . exec('git rev-parse HEAD') 17 | ])->appendContent(exec('git rev-parse --short HEAD')); 18 | 19 | $authorLink = (new OOUI\Tag('a'))->setAttributes([ 20 | 'href' => "https://en.wikipedia.org/wiki/User:BrandonXLF" 21 | ])->appendContent('BrandonXLF'); 22 | 23 | $parts = [ 24 | (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/"])->appendContent('Form'), 25 | (new OOUI\Tag('a'))->setAttributes(['href' => "./{$this->rel}/api/"])->appendContent('API'), 26 | "$gitHubLink ($revLink)", 27 | "Created by $authorLink" 28 | ]; 29 | 30 | $content = new OOUI\HtmlSnippet(implode(' | ', $parts)); 31 | 32 | return (new OOUI\Tag('footer'))->appendContent($content)->toString(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /includes/Form.php: -------------------------------------------------------------------------------- 1 | 'project', 9 | 'id' => 'project', 10 | 'value' => get('project'), 11 | 'default' => Config::get('default-project'), 12 | 'autocomplete' => false, 13 | 'infusable' => true 14 | ]), [ 15 | 'align' => 'top', 16 | 'label' => 'Project' 17 | ] 18 | ), 19 | new OOUI\FieldLayout( 20 | new PageLookupWidget([ 21 | 'name' => 'page', 22 | 'id' => 'page', 23 | 'value' => get('page'), 24 | 'autocomplete' => false, 25 | 'infusable' => true 26 | ]), [ 27 | 'align' => 'top', 28 | 'label' => 'Page' 29 | ] 30 | ), 31 | new OOUI\FieldLayout( 32 | new OOUI\TextInputWidget([ 33 | 'name' => 'namespaces', 34 | 'id' => 'namespaces', 35 | 'value' => get('namespaces'), 36 | 'placeholder' => 'Separate using commas', 37 | 'infusable' => true 38 | ]), [ 39 | 'align' => 'top', 40 | 'label' => 'Namespaces' 41 | ] 42 | ), 43 | new OOUI\FieldLayout( 44 | new OOUI\ButtonInputWidget([ 45 | 'id' => 'submit', 46 | 'type' => 'submit', 47 | 'label' => 'Submit', 48 | 'flags' => ['primary', 'progressive'], 49 | 'infusable' => true 50 | ]), [ 51 | 'align' => 'top' 52 | ] 53 | ) 54 | ]; 55 | 56 | return (new OOUI\FormLayout([ 57 | 'items' => $fields, 58 | 'id' => 'form' 59 | ]))->toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /includes/Global.php: -------------------------------------------------------------------------------- 1 | files = $files; 9 | } 10 | 11 | public function getContent() { 12 | $out = ''; 13 | 14 | if (!headers_sent()) { 15 | header("Content-Type: text/javascript"); 16 | } 17 | 18 | foreach ($this->files as $file) { 19 | $content = file_get_contents($file); 20 | $out .= "/* $file */\n{$content}\n"; 21 | } 22 | 23 | return $out; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /includes/JsonProducer.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'name' => 'File links', 16 | 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hidelinks=1' 17 | ], 18 | 'categorylinks' => [ 19 | 'name' => 'Category links', 20 | 'url' => '/wiki/PAGE' // WhatLinksHere doesn't show category links 21 | ], 22 | 'wikilinks' => [ 23 | 'name' => 'Wikilinks', 24 | 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidetrans=1&hideimages=1' 25 | ], 26 | 'redirects' => [ 27 | 'name' => 'Redirects', 28 | 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hidetrans=1&hideimages=1' 29 | ], 30 | 'transclusions' => [ 31 | 'name' => 'Transclusions', 32 | 'url' => '/wiki/Special:WhatLinksHere/PAGE?hidelinks=1&hideimages=1' 33 | ] 34 | ]; 35 | 36 | public function __construct(string $page, string $project, $namespaces = '') { 37 | if (!$page && !$project && $namespaces === '') { 38 | return; 39 | } 40 | 41 | if (!$page) { 42 | $this->error = 'Page name is required.'; 43 | return; 44 | } 45 | 46 | if (!$project) { 47 | $project = Config::get('default-project'); 48 | } 49 | 50 | foreach ($namespaces ? explode(',', $namespaces) : [] as $rawNamespace) { 51 | if (!is_numeric($rawNamespace)) { 52 | $this->error = 'Invalid namespace IDs.'; 53 | return; 54 | } 55 | } 56 | 57 | $maybeProjectURL = 'https://' . preg_replace('/^https:\/\//', '', $project); 58 | $metaDB = DatabaseFactory::create(); 59 | 60 | $stmt = $metaDB->prepare('SELECT dbname, url FROM wiki WHERE dbname=? OR url=? LIMIT 1'); 61 | $stmt->execute([$project, $maybeProjectURL]); 62 | 63 | if (!$stmt->rowCount()) { 64 | $this->error = 'That project does not exist...'; 65 | return; 66 | } 67 | 68 | list($dbName, $this->projectURL) = $stmt->fetch(); 69 | $metaDB = null; 70 | 71 | $db = DatabaseFactory::create($dbName); 72 | $this->title = new Title($page, $dbName, $this->projectURL); 73 | $this->countQuery = new CountQuery($namespaces, $db, $this->title); 74 | 75 | $this->counts = [ 76 | 'filelinks' => $this->title->getNamespaceId() === 6 77 | ? $this->countQuery->runQuery( 78 | 'imagelinks', 79 | 'il', 80 | CountQueryMode::Transclusion, 81 | CountQuery::SINGLE_NS | CountQuery::NO_LINK_TARGET 82 | ) 83 | : null, 84 | 'categorylinks' => $this->title->getNamespaceId() === 14 85 | ? $this->countQuery->runQuery( 86 | 'categorylinks', 87 | 'cl', 88 | CountQueryMode::Link, 89 | CountQuery::SINGLE_NS | CountQuery::NO_FROM_NS | CountQuery::NO_LINK_TARGET 90 | ) 91 | : null, 92 | 'wikilinks' => $this->countQuery->runQuery( 93 | 'pagelinks', 94 | 'pl', 95 | CountQueryMode::Link 96 | ), 97 | 'redirects' => $this->countQuery->runQuery( 98 | 'redirect', 99 | 'rd', 100 | CountQueryMode::Redirect, 101 | CountQuery::NO_FROM_NS | CountQuery::NO_LINK_TARGET 102 | ), 103 | 'transclusions' => $this->countQuery->runQuery( 104 | 'templatelinks', 105 | 'tl', 106 | CountQueryMode::Transclusion 107 | ) 108 | ]; 109 | 110 | // Redirects are included in the wikilinks table 111 | $this->counts['wikilinks']['all'] -= $this->counts['redirects']; 112 | $this->counts['wikilinks']['direct'] -= $this->counts['redirects']; 113 | } 114 | 115 | public function getTitle() { 116 | $parts = []; 117 | 118 | if (isset($this->error)) { 119 | array_push($parts, 'Error'); 120 | } elseif (isset($this->counts)) { 121 | array_push($parts, $this->title->getFullText()); 122 | } 123 | 124 | array_push($parts, 'Link Count'); 125 | 126 | return implode(' - ', $parts); 127 | } 128 | 129 | public function getHtml() { 130 | if (isset($this->error)) { 131 | return (new OOUI\Tag('div'))->addClasses(['error'])->appendContent($this->error)->toString(); 132 | } 133 | 134 | if (!isset($this->counts)) { 135 | return LinkCount::$description; 136 | } 137 | 138 | $validCounts = array_filter($this->counts, function($val) { 139 | return $val !== null; 140 | }); 141 | 142 | $out = (new OOUI\Tag('div'))->addClasses(['out'])->setAttributes([ 143 | 'role' => 'table', 144 | 'aria-rowcount' => count($validCounts) + 1 145 | ]); 146 | 147 | $out->appendContent( 148 | (new OOUI\Tag('div'))->setAttributes([ 149 | 'role' => 'row' 150 | ])->appendContent( 151 | (new OOUI\Tag('div'))->setAttributes([ 152 | 'role' => 'columnheader' 153 | ])->appendContent('Type'), 154 | (new OOUI\Tag('div'))->setAttributes([ 155 | 'role' => 'columnheader' 156 | ])->appendContent('All'), 157 | (new OOUI\Tag('abbr'))->setAttributes([ 158 | 'title' => 'Number of pages that link to page using the actual page name', 159 | 'role' => 'columnheader' 160 | ])->appendContent('Direct'), 161 | (new OOUI\Tag('abbr'))->setAttributes([ 162 | 'title' => 'Number of pages that link to the page through a redirect', 163 | 'role' => 'columnheader' 164 | ])->appendContent('Indirect') 165 | ) 166 | ); 167 | 168 | $encodedPage = rawurlencode($this->title->getFullText()); 169 | 170 | foreach ($validCounts as $key => $count) { 171 | $singleCount = is_int($count); 172 | 173 | $label = (new OOUI\Tag('a'))->setAttributes([ 174 | 'href' => $this->projectURL . str_replace('PAGE', $encodedPage, $this->typeInfo[$key]['url']) 175 | ])->appendContent($this->typeInfo[$key]['name']); 176 | 177 | $link = (new OOUI\Tag('a'))->addClasses(['hash-link'])->setAttributes([ 178 | 'href' => '#' . $key, 179 | 'title' => 'Link to row' 180 | ])->appendContent('(#)'); 181 | 182 | $all = number_format($singleCount ? $count : $count['all']); 183 | $direct = $singleCount ? new OOUI\HtmlSnippet('‒') : number_format($count['direct']); 184 | $indirect = $singleCount ? new OOUI\HtmlSnippet('‒') : number_format($count['indirect']); 185 | 186 | $out->appendContent( 187 | (new OOUI\Tag('div'))->setAttributes([ 188 | 'id' => $key, 189 | 'role' => 'row' 190 | ])->appendContent( 191 | (new OOUI\Tag('div'))->addClasses(['type'])->setAttributes([ 192 | 'role' => 'cell' 193 | ])->appendContent($label, ' ', $link), 194 | (new OOUI\Tag('div'))->addClasses(['all'])->setAttributes([ 195 | 'role' => 'cell' 196 | ])->appendContent($all), 197 | (new OOUI\Tag('div'))->addClasses(['direct'])->setAttributes([ 198 | 'role' => 'cell' 199 | ])->appendContent($direct), 200 | (new OOUI\Tag('div'))->addClasses(['indirect'])->setAttributes([ 201 | 'role' => 'cell' 202 | ])->appendContent($indirect) 203 | ) 204 | ); 205 | } 206 | 207 | $links = (new OOUI\Tag('div'))->addClasses(['links'])->appendContent( 208 | (new OOUI\Tag('a'))->setAttributes([ 209 | 'href' => $this->projectURL . '/wiki/Special:WhatLinksHere/' . $encodedPage 210 | ])->appendContent('What links here') 211 | ); 212 | 213 | return $out . $links; 214 | } 215 | 216 | public function getPageUpdateJson() { 217 | return json_encode([ 218 | 'title' => $this->getTitle(), 219 | 'html' => $this->getHtml() 220 | ]); 221 | } 222 | 223 | public function getJson() { 224 | if (isset($this->error)) { 225 | return json_encode(['error' => $this->error]); 226 | } 227 | 228 | return json_encode((object) $this->counts); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /includes/PageLookupWidget.php: -------------------------------------------------------------------------------- 1 | default = $config['default']; 13 | 14 | $project = $config['value']; 15 | 16 | if (!$project) { 17 | $this->domain = $this->default; 18 | return; 19 | } 20 | 21 | $db = DatabaseFactory::create(); 22 | $maybeProjectURL = 'https://' . preg_replace('/^https:\/\//', '', $project); 23 | 24 | $stmt = $db->prepare('SELECT url FROM wiki WHERE dbname = ? OR url = ? LIMIT 1'); 25 | $stmt->execute([$project, $maybeProjectURL]); 26 | $row = $stmt->fetch(); 27 | 28 | if ($row) { 29 | $this->domain = substr($row[0], 8); 30 | } 31 | } 32 | 33 | public function getConfig(&$config) { 34 | $config['domain'] = $this->domain; 35 | $config['default'] = $this->default; 36 | 37 | return parent::getConfig($config); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /includes/ProjectPrefixSearch.php: -------------------------------------------------------------------------------- 1 | projects = []; 10 | return; 11 | } 12 | 13 | $db = DatabaseFactory::create(); 14 | $maybeProjectURL = 'https://' . preg_replace('/^https:\/\//', '', $prefix); 15 | 16 | $stmt = $db->prepare('SELECT dbname, url FROM wiki WHERE dbname LIKE ? OR url LIKE ?'); 17 | $stmt->execute([$prefix . '%', $maybeProjectURL . '%']); 18 | 19 | foreach ($stmt->fetchAll() as $row) { 20 | $domain = substr($row[1], 8); 21 | 22 | if ($row[0] == $prefix || $row[1] == $maybeProjectURL) { 23 | $this->exact = $domain; 24 | } 25 | 26 | $this->projects[] = $domain; 27 | } 28 | } 29 | 30 | public function getJson() { 31 | return json_encode([ 32 | 'projects' => $this->projects, 33 | 'exact' => $this->exact 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /includes/Title.php: -------------------------------------------------------------------------------- 1 | databaseName = $databaseName; 13 | $this->projectURL = $projectURL; 14 | 15 | $text = strtr($text, '_', ' '); 16 | $text = trim($text); 17 | 18 | if ($text[0] == ':') { 19 | $text = substr($text, 1); 20 | } 21 | 22 | list($maybeNamespace, $title) = $this->breakupText($text); 23 | 24 | $namespaceInfo = $this->getNamespaceInfo($maybeNamespace); 25 | 26 | if (!$namespaceInfo) { 27 | $namespaceInfo = $this->getNamespaceInfo(''); 28 | $title = $text; 29 | } 30 | 31 | $this->title = $namespaceInfo[2] ? ucfirst($title) : $title; 32 | $this->namespaceInfo = $namespaceInfo; 33 | } 34 | 35 | public function getNamespaceId(): int { 36 | return $this->namespaceInfo[0]; 37 | } 38 | 39 | public function getDBKey(): string { 40 | return strtr($this->title, ' ', '_'); 41 | } 42 | 43 | public function getFullText(): string { 44 | if ($this->namespaceInfo[1] == '') { 45 | return $this->title; 46 | } 47 | 48 | return "{$this->namespaceInfo[1]}:{$this->title}"; 49 | } 50 | 51 | private function breakupText(string $text) { 52 | if (strpos($text, ':') === false) { 53 | return ['', $text]; 54 | } else { 55 | return explode(':', $text, 2); 56 | } 57 | } 58 | 59 | private function getNamespaceInfo(string $namespace): array|null { 60 | $redis = new Redis; 61 | 62 | $redis->connect(Config::get('redis-server'), Config::get('redis-port')); 63 | 64 | $redisAuth = Config::get('redis-auth'); 65 | 66 | if ($redisAuth) { 67 | $redis->auth($redisAuth); 68 | } 69 | 70 | $prefix = Config::get('redis-prefix'); 71 | $ver = 'v' . self::REDIS_DB_VER; 72 | $nsInfoHashKey = "$prefix:$ver:{$this->databaseName}"; 73 | 74 | if (!$redis->exists($nsInfoHashKey)) { 75 | $namespaceByName = $this->getNamespaceInfoStrings(); 76 | 77 | $redis->hMSet($nsInfoHashKey, $namespaceByName); 78 | $redis->expire($nsInfoHashKey, 86400); 79 | } 80 | 81 | $namespaceInfoString = $redis->hGet($nsInfoHashKey, strtolower($namespace)); 82 | 83 | $redis->close(); 84 | 85 | if (!$namespaceInfoString) { 86 | return null; 87 | } 88 | 89 | return json_decode($namespaceInfoString); 90 | } 91 | 92 | private function getNamespaceInfoStrings() { 93 | $curl = curl_init(); 94 | 95 | curl_setopt_array($curl, [ 96 | CURLOPT_URL => $this->projectURL . '/w/api.php?action=query&meta=siteinfo&siprop=namespaces|namespacealiases&format=json&formatversion=2', 97 | CURLOPT_RETURNTRANSFER => true, 98 | CURLOPT_USERAGENT => Config::get('useragent') 99 | ]); 100 | 101 | $info = json_decode(curl_exec($curl)); 102 | 103 | curl_close($curl); 104 | 105 | $namespaceByName = []; 106 | $namespaceById = []; 107 | 108 | foreach ($info->query->namespaces as $namespace) { 109 | $nsInfoString = json_encode([ 110 | $namespace->id, 111 | $namespace->name, 112 | $namespace->case == 'first-letter', 113 | ]); 114 | 115 | $namespaceByName[strtolower($namespace->name)] = $nsInfoString; 116 | 117 | if (isset($namespace->canonical)) { 118 | $namespaceByName[strtolower($namespace->canonical)] = $nsInfoString; 119 | } 120 | 121 | $namespaceById[$namespace->id] = $nsInfoString; 122 | } 123 | 124 | foreach ($info->query->namespacealiases as $namespace) { 125 | $namespaceByName[strtolower($namespace->alias)] = $namespaceById[$namespace->id]; 126 | } 127 | 128 | return $namespaceByName; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | <?php echo $linkCount->getTitle(); ?> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 |

    Link Count

    22 | 23 | getHtml(); ?> 24 |
    getHtml(); ?>
    25 |
    26 | getHTML(); ?> 27 | 28 | 29 | -------------------------------------------------------------------------------- /js/index.php: -------------------------------------------------------------------------------- 1 | getContent(); 15 | -------------------------------------------------------------------------------- /output/index.php: -------------------------------------------------------------------------------- 1 | getPageUpdateJson(); 9 | 10 | ?> 11 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkcount", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "jquery": "^3.7.1", 9 | "oojs": "^7.0.1", 10 | "oojs-ui": "^0.48.2", 11 | "wikimedia-ui-base": "^0.22.0" 12 | } 13 | }, 14 | "node_modules/jquery": { 15 | "version": "3.7.1", 16 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", 17 | "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" 18 | }, 19 | "node_modules/oojs": { 20 | "version": "7.0.1", 21 | "resolved": "https://registry.npmjs.org/oojs/-/oojs-7.0.1.tgz", 22 | "integrity": "sha512-DDN+B6PY29na74CyAB4bl0FgBM3ze31wd7LICghoWkd+SXrvn0W0GA1HurYdVESfBoIokqLuejk6yrbtq4PTTA==" 23 | }, 24 | "node_modules/oojs-ui": { 25 | "version": "0.48.2", 26 | "resolved": "https://registry.npmjs.org/oojs-ui/-/oojs-ui-0.48.2.tgz", 27 | "integrity": "sha512-p7aT/RfXgU/YiGQsCHnYDzW6JRoPdBHmYUG862ALvj0+xRPMtqX7bMN8PhnMtntEzVSD4y+DADy9DrAss3fVwQ==", 28 | "dependencies": { 29 | "jquery": "3.7.1", 30 | "oojs": "7.0.1", 31 | "wikimedia-ui-base": "0.22.0" 32 | } 33 | }, 34 | "node_modules/wikimedia-ui-base": { 35 | "version": "0.22.0", 36 | "resolved": "https://registry.npmjs.org/wikimedia-ui-base/-/wikimedia-ui-base-0.22.0.tgz", 37 | "integrity": "sha512-2aaOF5iNvXRsyJnKjNEASuHcE4XXjxYlOwSfRd5uldLUYFevDA5FhwPTWKpG9IJG1LfFvsr3skPdFvp0sSpYgQ==" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jquery": "^3.7.1", 4 | "oojs": "^7.0.1", 5 | "oojs-ui": "^0.48.2", 6 | "wikimedia-ui-base": "^0.22.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/index.php: -------------------------------------------------------------------------------- 1 | getJson(); 7 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /*?* 3 | -------------------------------------------------------------------------------- /static/NamespaceLookupWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Select element for namespaces of an object 3 | * 4 | * @constructor 5 | * @param {object} config Configuration options 6 | */ 7 | function NamespaceLookupWidget(config) { 8 | config.allowArbitrary = true; 9 | NamespaceLookupWidget.super.call(this, config); 10 | 11 | this.setValue(config.value); 12 | this.setDomain(config.domain); 13 | } 14 | 15 | OO.inheritClass(NamespaceLookupWidget, OO.ui.MenuTagMultiselectWidget); 16 | 17 | NamespaceLookupWidget.prototype.clearMenu = function() { 18 | let oldValues = this.getValue(); 19 | 20 | this.clearItems(); 21 | this.getMenu().clearItems(); 22 | 23 | this.allowArbitrary = true; 24 | this.setValue(oldValues); 25 | } 26 | 27 | NamespaceLookupWidget.prototype.setDomain = function(domain) { 28 | if (!domain) { 29 | this.clearMenu(); 30 | return; 31 | } 32 | 33 | return $.get('https://' + domain + '/w/api.php', { 34 | action: 'query', 35 | meta: 'siteinfo', 36 | siprop: 'namespaces', 37 | format: 'json', 38 | origin: '*', 39 | formatversion: 2 40 | }).then(function(res) { 41 | let oldValues = this.getValue(), 42 | options = []; 43 | 44 | this.clearItems(); 45 | this.getMenu().clearItems(); 46 | 47 | for (let id in res.query.namespaces) { 48 | if (id < 0) continue; // Ignore virtual namespaces 49 | 50 | let info = res.query.namespaces[id]; 51 | 52 | options.push({ 53 | data: info.id.toString(), 54 | label: info.name || '(Article)' 55 | }); 56 | } 57 | 58 | this.allowArbitrary = false; 59 | 60 | this.addOptions(options); 61 | this.setValue(oldValues); 62 | }.bind(this), this.clearMenu.bind(this)); 63 | } 64 | 65 | NamespaceLookupWidget.prototype.isAllowedData = function(data) { 66 | return NamespaceLookupWidget.super.prototype.isAllowedData.call(this, data) && /^-?[0-9]+$/.test(data); 67 | } 68 | -------------------------------------------------------------------------------- /static/PageLookupWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lookup element for pages of a project 3 | * setProject must be called for lookup to start 4 | * 5 | * @constructor 6 | * @param {object} config Configuration options 7 | */ 8 | function PageLookupWidget(config) { 9 | PageLookupWidget.super.call(this, config); 10 | OO.ui.mixin.LookupElement.call(this, config); 11 | 12 | this.setDomain(config.domain); 13 | } 14 | 15 | OO.inheritClass(PageLookupWidget, OO.ui.TextInputWidget); 16 | OO.mixinClass(PageLookupWidget, OO.ui.mixin.LookupElement); 17 | 18 | PageLookupWidget.prototype.getLookupRequest = function() { 19 | return this.domain ? $.get('https://' + this.domain + '/w/api.php', { 20 | action: 'query', 21 | generator: 'prefixsearch', 22 | gpssearch: this.getValue(), 23 | gpslimit: 10, 24 | origin: '*', 25 | format: 'json', 26 | formatversion: 2 27 | }) : $.Deferred().resolve({ 28 | query: { 29 | pages: [] 30 | } 31 | }).promise(); 32 | }; 33 | 34 | PageLookupWidget.prototype.getLookupCacheDataFromResponse = function(res) { 35 | var titles = res.query.pages || []; 36 | 37 | titles.sort(function(a, b) { 38 | return a.index - b.index; 39 | }); 40 | 41 | return titles; 42 | }; 43 | 44 | PageLookupWidget.prototype.getLookupMenuOptionsFromData = function(titles) { 45 | return titles.map(function(value) { 46 | return new OO.ui.MenuOptionWidget({ 47 | data: value.title, 48 | label: value.title 49 | }); 50 | }) 51 | }; 52 | 53 | PageLookupWidget.prototype.setDomain = function(domain) { 54 | this.domain = domain; 55 | }; 56 | 57 | PageLookupWidget.prototype.getRequestQuery = function() { 58 | return this.domain + '/' + this.getValue(); 59 | }; 60 | -------------------------------------------------------------------------------- /static/ProjectLookupWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lookup element for project names 3 | * 4 | * @constructor 5 | * @param {object} config Configuration options 6 | */ 7 | function ProjectLookupWidget(config) { 8 | ProjectLookupWidget.super.call(this, config); 9 | OO.ui.mixin.LookupElement.call(this, config); 10 | 11 | this.domain = config.domain; 12 | this.default = config.default; 13 | 14 | this.lookupMenu.connect(this, { 15 | choose: 'onProjectLookupMenuChoose' 16 | }); 17 | } 18 | 19 | OO.inheritClass(ProjectLookupWidget, OO.ui.TextInputWidget); 20 | OO.mixinClass(ProjectLookupWidget, OO.ui.mixin.LookupElement); 21 | 22 | ProjectLookupWidget.prototype.getLookupRequest = function() { 23 | return $.get('projects/', { 24 | prefix: this.getValue() 25 | }); 26 | }; 27 | 28 | ProjectLookupWidget.prototype.getLookupCacheDataFromResponse = function(response) { 29 | return response || []; 30 | }; 31 | 32 | ProjectLookupWidget.prototype.getLookupMenuOptionsFromData = function(data) { 33 | this.setDomain(data.exact); 34 | 35 | return data.projects.map(function(value) { 36 | return new OO.ui.MenuOptionWidget({ 37 | data: value, 38 | label: value 39 | }); 40 | }); 41 | }; 42 | 43 | ProjectLookupWidget.prototype.onProjectLookupMenuChoose = function(item) { 44 | this.setDomain(item.getData()); 45 | }; 46 | 47 | ProjectLookupWidget.prototype.getValue = function() { 48 | return this.value || this.default; 49 | }; 50 | 51 | ProjectLookupWidget.prototype.getDomain = function() { 52 | return this.domain; 53 | } 54 | 55 | ProjectLookupWidget.prototype.setDomain = function(domain) { 56 | if (domain == this.domain) return; 57 | 58 | this.domain = domain; 59 | this.emit('domain', domain); 60 | } 61 | 62 | ProjectLookupWidget.prototype.getValidity = function() { 63 | var deferred = $.Deferred(); 64 | 65 | if (this.domain) { 66 | deferred.resolve(); 67 | } else { 68 | deferred.reject(); 69 | } 70 | 71 | return deferred.promise(); 72 | } 73 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrandonXLF/linkcount/421170e23de5b42e4bcd2a4f59ef633d2eea8376/static/icon.png -------------------------------------------------------------------------------- /static/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | line-height: 1.5; 4 | background: #f0f0f0; 5 | } 6 | 7 | abbr { 8 | cursor: help; 9 | } 10 | 11 | #skip { 12 | left: -99999px; 13 | position: absolute; 14 | } 15 | 16 | #skip:focus { 17 | top: 8px; 18 | left: 8px; 19 | border: 2px solid #36c; 20 | border-radius: 4px; 21 | outline: none; 22 | text-decoration: underline; 23 | background: #fff; 24 | padding: 4px; 25 | } 26 | 27 | main, 28 | footer { 29 | max-width: 700px; 30 | margin: 0 auto; 31 | } 32 | 33 | .links { 34 | margin-top: 8px; 35 | } 36 | 37 | footer, 38 | #out { 39 | margin-top: 1.5em; 40 | } 41 | 42 | .error { 43 | color: #e00000; 44 | } 45 | 46 | .oo-ui-widget.oo-ui-progressBarWidget { 47 | border-radius: 2px; 48 | box-shadow: none; 49 | } 50 | 51 | .out { 52 | display: grid; 53 | grid-template-columns: 1fr 1fr 1fr 1fr; 54 | font-size: 125%; 55 | } 56 | 57 | .out a { 58 | text-decoration: none; 59 | } 60 | 61 | .out [role="row"] { 62 | display: contents; 63 | } 64 | 65 | .out [role="row"] > * { 66 | display: inline-block; 67 | } 68 | 69 | .out [role="columnheader"] { 70 | font-weight: bold; 71 | padding: 8px 12px 0; 72 | text-align: center; 73 | } 74 | 75 | .out [role="cell"] { 76 | border: solid #a2a9b1; 77 | background: #fff; 78 | border-width: 1px 0 1px 1px; 79 | padding: 12px 8px; 80 | margin: 8px 0; 81 | } 82 | 83 | .out .type { 84 | border-radius: 2px 0 0 2px; 85 | white-space: nowrap; 86 | display: inline-flex; 87 | align-items: center; 88 | gap: 6px; 89 | } 90 | 91 | .hash-link { 92 | color: #5d5f62; 93 | font-size: 70%; 94 | } 95 | 96 | .out .all, 97 | .out .direct, 98 | .out .indirect { 99 | font-size: 140%; 100 | line-height: 107%; 101 | text-align: right; 102 | } 103 | 104 | .out .indirect { 105 | border-right-width: 1px; 106 | border-radius: 0 2px 2px 0; 107 | } 108 | 109 | .out [role="row"]:target [role="cell"] { 110 | padding: 10px 8px; 111 | border-top-width: 3px; 112 | border-bottom-width: 3px; 113 | border-color: #36c; 114 | } 115 | 116 | .out [role="row"]:target .type { 117 | padding: 10px 10px 10px 6px; 118 | border-left-width: 3px; 119 | border-radius: 4px 0 0 4px; 120 | } 121 | 122 | .out [role="row"]:target .indirect { 123 | padding: 10px 6px 10px 10px; 124 | border-right-width: 3px; 125 | border-radius: 0 4px 4px 0; 126 | } 127 | 128 | @media (max-width: 575px) { 129 | .out { 130 | display: grid; 131 | grid-template-columns: 1fr 1fr 1fr; 132 | } 133 | 134 | .out > :first-child > :first-child { 135 | display: none; 136 | } 137 | 138 | .out .type { 139 | grid-column-start: 1; 140 | grid-column-end: 4; 141 | border-width: 1px; 142 | border-radius: 2px 2px 0 0; 143 | margin-bottom: 0; 144 | line-height: 80%; 145 | justify-content: center; 146 | } 147 | 148 | .out .all, 149 | .out .direct, 150 | .out .indirect { 151 | border-top: none; 152 | margin-top: 0; 153 | font-size: 125%; 154 | } 155 | 156 | .out .all { 157 | border-left-width: 1px; 158 | border-radius: 0 0 0 2px; 159 | } 160 | 161 | .out .indirect { 162 | border-radius: 0 0 2px 0; 163 | } 164 | 165 | .out [role="row"]:target .type { 166 | padding: 10px 8px 12px 8px; 167 | border-right-width: 3px; 168 | border-bottom-width: 1px; 169 | border-radius: 4px 4px 0 0; 170 | } 171 | 172 | .out [role="row"]:target .all, 173 | .out [role="row"]:target .direct, 174 | .out [role="row"]:target .indirect { 175 | padding: 12px 8px 10px 8px; 176 | } 177 | 178 | .out [role="row"]:target .all { 179 | border-left-width: 3px; 180 | border-radius: 0 0 0 4px; 181 | } 182 | 183 | .out [role="row"]:target .indirect { 184 | padding: 12px 6px 10px 10px; 185 | border-radius: 0 0 4px 0; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | let projectLookup = OO.ui.infuse($('#project')), 2 | pageLookup = OO.ui.infuse($('#page'), { 3 | domain: projectLookup.getDomain() 4 | }), 5 | namespacesInput = OO.ui.infuse($('#namespaces')), 6 | button = OO.ui.infuse($('#submit')), 7 | namespacesSelect = new NamespaceLookupWidget({ 8 | value: namespacesInput.getValue() && namespacesInput.getValue().split(','), 9 | domain: projectLookup.getDomain() 10 | }), 11 | progressWidget = new OO.ui.ProgressBarWidget(), 12 | progressLayout = new OO.ui.FieldLayout(progressWidget, { 13 | align: 'top' 14 | }), 15 | out = $('#out'), 16 | request, 17 | currentSearch = location.search; 18 | 19 | function submitForm(pushState) { 20 | let params = { 21 | project: projectLookup.getValue(), 22 | page: pageLookup.getValue(), 23 | namespaces: namespacesSelect.getValue().join(',') 24 | }, 25 | query = Object.keys(params) 26 | .filter(param => params[param]) 27 | .map(param => param + '=' + encodeURIComponent(params[param])) 28 | .join('&'); 29 | 30 | if (pushState) { 31 | currentSearch = (query ? '?' : '') + query; 32 | history.pushState({}, null, currentSearch); 33 | } 34 | 35 | if (!query) { 36 | out.empty(); 37 | return; 38 | } 39 | 40 | if (request) { 41 | request.wasReplaced = true; 42 | request.abort(); 43 | } 44 | 45 | out.html(progressLayout.$element); 46 | 47 | request = $.get('output/?' + query); 48 | 49 | request.then(function(res) { 50 | document.title = res.title; 51 | out.html(res.html); 52 | }, function(req) { 53 | if (req.wasReplaced) return; 54 | 55 | out.html('
    Failed to send API request.
    '); 56 | }); 57 | } 58 | 59 | projectLookup.on('domain', function (domain) { 60 | pageLookup.setDomain(domain); 61 | namespacesSelect.setDomain(domain); 62 | }); 63 | 64 | button.on('click', function() { 65 | submitForm(true); 66 | }); 67 | 68 | window.addEventListener('popstate', function() { 69 | if (currentSearch === location.search) return; 70 | currentSearch = location.search; 71 | 72 | let params = {}; 73 | 74 | currentSearch.slice(1).split('&').forEach(param => { 75 | let chunks = param.split('='), 76 | key = chunks.shift(), 77 | value = decodeURIComponent(chunks.join('=')); 78 | 79 | params[key] = value; 80 | }); 81 | 82 | projectLookup.setValue(params.project || ''); 83 | pageLookup.setValue(params.page || ''); 84 | namespacesSelect.setValue((params.namespaces || '').split(',')); 85 | 86 | submitForm(false); 87 | }); 88 | 89 | $('#skip').on('click', function(e) { 90 | e.preventDefault(); 91 | out.trigger('focus'); 92 | }); 93 | 94 | namespacesInput.$element.replaceWith(namespacesSelect.$element); 95 | -------------------------------------------------------------------------------- /tests/LinkCountTest.php: -------------------------------------------------------------------------------- 1 | self::$db->prepare('INSERT INTO `page` (page_id, page_namespace, page_title) VALUES (?, ?, ?)'), 21 | 'redirect' => self::$db->prepare('INSERT INTO redirect (rd_from, rd_namespace, rd_title, rd_interwiki) VALUES (?, ?, ?, NULL)'), 22 | 'iwredirect' => self::$db->prepare('INSERT INTO redirect (rd_from, rd_namespace, rd_title, rd_interwiki) VALUES (?, ?, ?, ?)'), 23 | 'pagelink' => self::$db->prepare('INSERT INTO pagelinks (pl_from, pl_from_namespace, pl_target_id) VALUES (?, ?, ?)'), 24 | 'templatelink' => self::$db->prepare('INSERT INTO templatelinks (tl_from, tl_from_namespace, tl_target_id) VALUES (?, ?, ?)'), 25 | 'categorylink' => self::$db->prepare('INSERT INTO categorylinks (cl_from, cl_to) VALUES (?, ?)'), 26 | 'imagelink' => self::$db->prepare('INSERT INTO imagelinks (il_from, il_to, il_from_namespace) VALUES (?, ?, ?)'), 27 | 'linktarget' => self::$db->prepare('INSERT INTO linktarget (lt_id, lt_namespace, lt_title) VALUES (?, ?, ?)') 28 | ]; 29 | 30 | self::$defaultExpected = [ 31 | 'filelinks' => null, 32 | 'categorylinks' => null, 33 | 'wikilinks' => [0,0,0], 34 | 'redirects' => 0, 35 | 'transclusions' => [0,0,0] 36 | ]; 37 | 38 | self::$db->exec(file_get_contents(__DIR__ . '/createdb.sql')); 39 | } 40 | 41 | private function ensurePage($ns, $title) { 42 | if (!array_key_exists($ns, $this->pageIDs)) { 43 | $this->pageIDs[$ns] = []; 44 | } 45 | 46 | if (!array_key_exists($title, $this->pageIDs[$ns])) { 47 | $id = ++$this->counter; 48 | self::$statements['page']->execute([$id, $ns, $title]); 49 | $this->pageIDs[$ns][$title] = $id; 50 | } 51 | 52 | return $this->pageIDs[$ns][$title]; 53 | } 54 | 55 | private function ensureTarget($ns, $title) { 56 | if (!array_key_exists($ns, $this->targetIDs)) { 57 | $this->targetIDs[$ns] = []; 58 | } 59 | 60 | if (!array_key_exists($title, $this->targetIDs[$ns])) { 61 | $id = ++$this->counter; 62 | self::$statements['linktarget']->execute([$id, $ns, $title]); 63 | $this->targetIDs[$ns][$title] = $id; 64 | } 65 | 66 | return $this->targetIDs[$ns][$title]; 67 | } 68 | 69 | private function addRedirect($pagelink, $fromNS, $fromTitle, $toNS, $toTitle, $iw = null) { 70 | $fromID = $this->ensurePage($fromNS, $fromTitle); 71 | 72 | if ($iw !== null) { 73 | self::$statements['iwredirect']->execute([$fromID, $toNS, $toTitle, $iw]); 74 | } else { 75 | self::$statements['redirect']->execute([$fromID, $toNS, $toTitle]); 76 | } 77 | 78 | if ($pagelink) { 79 | $this->addPageLink($fromNS, $fromTitle, $toNS, $toTitle); 80 | } 81 | } 82 | 83 | private function addPageLink($fromNS, $fromTitle, $toNS, $toTitle) { 84 | $fromID = $this->ensurePage($fromNS, $fromTitle); 85 | $targetID = $this->ensureTarget($toNS, $toTitle); 86 | self::$statements['pagelink']->execute([$fromID, $fromNS, $targetID]); 87 | } 88 | 89 | private function addTemplateLink($fromNS, $fromTitle, $toNS, $toTitle) { 90 | $fromID = $this->ensurePage($fromNS, $fromTitle); 91 | $targetID = $this->ensureTarget($toNS, $toTitle); 92 | self::$statements['templatelink']->execute([$fromID, $fromNS, $targetID]); 93 | } 94 | 95 | private function addCategoryLink($fromNS, $fromTitle, $toTitle) { 96 | $fromID = $this->ensurePage($fromNS, $fromTitle); 97 | self::$statements['categorylink']->execute([$fromID, $toTitle]); 98 | } 99 | 100 | private function addImageLink($fromNS, $fromTitle, $toTitle) { 101 | $fromID = $this->ensurePage($fromNS, $fromTitle); 102 | self::$statements['imagelink']->execute([$fromID, $toTitle, $fromNS]); 103 | } 104 | 105 | private function compareCounts($page, $expected, $namespaces = '') { 106 | $expected += self::$defaultExpected; 107 | $actual = (new LinkCount($page, 'linkcounttest', $namespaces))->counts; 108 | 109 | foreach ($expected as $key => $val) { 110 | if (is_array($val)) { 111 | $expected[$key] = [ 112 | 'all' => $val[0], 113 | 'direct' => $val[1], 114 | 'indirect' => $val[2] 115 | ]; 116 | } 117 | } 118 | 119 | $this->assertEquals($expected, $actual, "page = $page, namespaces = $namespaces"); 120 | } 121 | 122 | /** 123 | * @dataProvider provideCounts 124 | */ 125 | public function testCounts($links, $counts, $nscounts = []) { 126 | self::$db->exec(" 127 | TRUNCATE TABLE categorylinks; 128 | TRUNCATE TABLE imagelinks; 129 | TRUNCATE TABLE page; 130 | TRUNCATE TABLE pagelinks; 131 | TRUNCATE TABLE redirect; 132 | TRUNCATE TABLE templatelinks; 133 | TRUNCATE TABLE linktarget; 134 | "); 135 | 136 | $this->pageIDs = []; 137 | $this->targetIDs = []; 138 | $this->counter = 0; 139 | 140 | if (array_key_exists('redirects+pagelinks', $links)) { 141 | foreach($links['redirects+pagelinks'] as $link) { 142 | $this->addRedirect(true, ...$link); 143 | } 144 | } 145 | 146 | if (array_key_exists('redirects', $links)) { 147 | foreach($links['redirects'] as $link) { 148 | $this->addRedirect(false, ...$link); 149 | } 150 | } 151 | 152 | if (array_key_exists('pagelinks', $links)) { 153 | foreach($links['pagelinks'] as $link) { 154 | $this->addPageLink(...$link); 155 | } 156 | } 157 | 158 | if (array_key_exists('templatelinks', $links)) { 159 | foreach($links['templatelinks'] as $link) { 160 | $this->addTemplateLink(...$link); 161 | } 162 | } 163 | 164 | if (array_key_exists('categorylinks', $links)) { 165 | foreach($links['categorylinks'] as $link) { 166 | $this->addCategoryLink(...$link); 167 | } 168 | } 169 | 170 | if (array_key_exists('imagelinks', $links)) { 171 | foreach($links['imagelinks'] as $link) { 172 | $this->addImageLink(...$link); 173 | } 174 | } 175 | 176 | foreach ($counts as $page => $expected) { 177 | $this->compareCounts($page, $expected); 178 | } 179 | 180 | foreach ($nscounts as $ns => $counts) { 181 | foreach ($counts as $page => $expected) { 182 | $this->compareCounts($page, $expected, $ns); 183 | } 184 | } 185 | } 186 | 187 | public function provideCounts() { 188 | return [ 189 | 'page without links' => [ 190 | [ 191 | 'pages' => [ 192 | ['pages', 0, 'Page'] 193 | ] 194 | ], 195 | [ 196 | 'Page' => [] 197 | ] 198 | ], 199 | 200 | 'redirects' => [ 201 | [ 202 | 'redirects+pagelinks' => [ 203 | [0, 'Redirect', 0, 'Page'], 204 | [0, 'Another_redirect', 0, 'Page'] 205 | ], 206 | ], 207 | [ 208 | 'Page' => [ 209 | 'redirects' => 2 210 | ] 211 | ] 212 | ], 213 | 'redirect with interwiki' => [ 214 | [ 215 | 'redirects' => [ 216 | [0, 'Redirect', 0, 'Page', 'en'] 217 | ] 218 | ], 219 | [ 220 | 'Page' => [ 221 | 'redirects' => 0 222 | ] 223 | ] 224 | ], 225 | 'redirect with blank interwiki' => [ 226 | [ 227 | 'redirects+pagelinks' => [ 228 | [0, 'Redirect', 0, 'Page', ''] 229 | ] 230 | ], 231 | [ 232 | 'Page' => [ 233 | 'redirects' => 1 234 | ] 235 | ] 236 | ], 237 | 238 | 'direct wikilink' => [ 239 | [ 240 | 'pagelinks' => [ 241 | [0, 'Link', 0, 'Page'] 242 | ], 243 | ], 244 | [ 245 | 'Page' => [ 246 | 'wikilinks' => [1,1,0] 247 | ] 248 | ] 249 | ], 250 | 'direct category link' => [ 251 | [ 252 | 'categorylinks' => [ 253 | [0, 'Link', 'Category'] 254 | ], 255 | ], 256 | [ 257 | 'Category:Category' => [ 258 | 'categorylinks' => [1,1,0] 259 | ] 260 | ] 261 | ], 262 | 'direct transclusion' => [ 263 | [ 264 | 'templatelinks' => [ 265 | [0, 'Link', 10, 'Template'] 266 | ] 267 | ], 268 | [ 269 | 'Template:Template' => [ 270 | 'transclusions' => [1,1,0] 271 | ] 272 | ] 273 | ], 274 | 'direct file link' => [ 275 | [ 276 | 'imagelinks' => [ 277 | [0, 'Link', 'Image.png'] 278 | ], 279 | ], 280 | [ 281 | 'File:Image.png' => [ 282 | 'filelinks' => [1,1,0] 283 | ] 284 | ] 285 | ], 286 | 287 | 'wikilink to redirect' => [ 288 | [ 289 | 'redirects+pagelinks' => [ 290 | [0, 'Redirect', 0, 'Page'] 291 | ], 292 | 'pagelinks' => [ 293 | [0, 'Link', 0, 'Redirect'] 294 | ] 295 | ], 296 | [ 297 | 'Redirect' => [ 298 | 'wikilinks' => [1,1,0] 299 | ], 300 | 'Page' => [ 301 | 'wikilinks' => [1,0,1], 302 | 'redirects' => 1 303 | ] 304 | ] 305 | ], 306 | 'category links to redirect' => [ 307 | [ 308 | 'redirects+pagelinks' => [ 309 | [14, 'Redirect', 14, 'Category'] 310 | ], 311 | 'categorylinks' => [ 312 | [0, 'Link', 'Redirect'] 313 | ] 314 | ], 315 | [ 316 | 'Category:Redirect' => [ 317 | 'categorylinks' => [1,1,0] 318 | ], 319 | 'Category:Category' => [ 320 | 'categorylinks' => [1,0,1], 321 | 'redirects' => 1 322 | ] 323 | ] 324 | ], 325 | 'transclusion of redirect' => [ 326 | [ 327 | 'redirects+pagelinks' => [ 328 | [10, 'Redirect', 10, 'Template'] 329 | ], 330 | 'templatelinks' => [ 331 | [0, 'Link', 10, 'Redirect'], 332 | // Count transclusion of redirect as a transclusion of the redirect target 333 | [0, 'Link', 10, 'Template'] 334 | ] 335 | ], 336 | [ 337 | 'Template:Redirect' => [ 338 | 'transclusions' => [1,1,0] 339 | ], 340 | 'Template:Template' => [ 341 | 'transclusions' => [1,0,1], 342 | 'redirects' => 1 343 | ] 344 | ] 345 | ], 346 | 'file link to redirect' => [ 347 | [ 348 | 'redirects+pagelinks' => [ 349 | [6, 'Redirect.png', 6, 'Image.png'] 350 | ], 351 | 'imagelinks' => [ 352 | [0, 'Link', 'Redirect.png'], 353 | // Count file link to redirect as a file link to the redirect target 354 | [0, 'Link', 'Image.png'] 355 | ] 356 | ], 357 | [ 358 | 'File:Redirect.png' => [ 359 | 'filelinks' => [1,1,0] 360 | ], 361 | 'File:Image.png' => [ 362 | 'filelinks' => [1,0,1], 363 | 'redirects' => 1 364 | ] 365 | ] 366 | ], 367 | 368 | 'transclusion of redirect content' => [ 369 | [ 370 | 'redirects+pagelinks' => [ 371 | [10, 'Redirect', 10, 'Template'] 372 | ], 373 | 'templatelinks' => [ 374 | [0, 'Link', 10, 'Redirect'] 375 | ] 376 | ], 377 | [ 378 | 'Template:Redirect' => [ 379 | 'transclusions' => [1,1,0] 380 | ], 381 | 'Template:Template' => [ 382 | 'transclusions' => [0,0,0], 383 | 'redirects' => 1 384 | ] 385 | ] 386 | ], 387 | 'file link to redirect content' => [ 388 | [ 389 | 'redirects+pagelinks' => [ 390 | [6, 'Redirect.png', 6, 'Image.png'] 391 | ], 392 | 'imagelinks' => [ 393 | [0, 'Link', 'Redirect.png'] 394 | ] 395 | ], 396 | [ 397 | 'File:Redirect.png' => [ 398 | 'filelinks' => [1,1,0] 399 | ], 400 | 'File:Image.png' => [ 401 | 'filelinks' => [0,0,0], 402 | 'redirects' => 1 403 | ] 404 | ] 405 | ], 406 | 407 | 'wikilinks to two redirects' => [ 408 | [ 409 | 'redirects+pagelinks' => [ 410 | [0, 'Redirect', 0, 'Page'], 411 | [0, 'Another_redirect', 0, 'Page'] 412 | ], 413 | 'pagelinks' => [ 414 | [0, 'Link', 0, 'Redirect'], 415 | [0, 'Link', 0, 'Another_redirect'] 416 | ] 417 | ], 418 | [ 419 | 'Redirect' => [ 420 | 'wikilinks' => [1,1,0] 421 | ], 422 | 'Another_redirect' => [ 423 | 'wikilinks' => [1,1,0] 424 | ], 425 | 'Page' => [ 426 | 'wikilinks' => [1,0,1], 427 | 'redirects' => 2, 428 | ] 429 | ] 430 | ], 431 | 'category links to two redirects' => [ 432 | [ 433 | 'redirects+pagelinks' => [ 434 | [14, 'Redirect', 14, 'Category'], 435 | [14, 'Another_redirect', 14, 'Category'] 436 | ], 437 | 'categorylinks' => [ 438 | [0, 'Link', 'Redirect'], 439 | [0, 'Link', 'Another_redirect'] 440 | ] 441 | ], 442 | [ 443 | 'Category:Redirect' => [ 444 | 'categorylinks' => [1,1,0] 445 | ], 446 | 'Category:Another_redirect' => [ 447 | 'categorylinks' => [1,1,0] 448 | ], 449 | 'Category:Category' => [ 450 | 'categorylinks' => [1,0,1], 451 | 'redirects' => 2, 452 | ] 453 | ] 454 | ], 455 | 'transclusions to two redirects' => [ 456 | [ 457 | 'redirects+pagelinks' => [ 458 | [10, 'Redirect', 10, 'Template'], 459 | [10, 'Another_redirect', 10, 'Template'] 460 | ], 461 | 'templatelinks' => [ 462 | [0, 'Link', 10, 'Redirect'], 463 | [0, 'Link', 10, 'Another_redirect'], 464 | // Count transclusion of redirect as a transclusion of the redirect target 465 | [0, 'Link', 10, 'Template'], 466 | ] 467 | ], 468 | [ 469 | 'Template:Redirect' => [ 470 | 'transclusions' => [1,1,0] 471 | ], 472 | 'Template:Another_redirect' => [ 473 | 'transclusions' => [1,1,0] 474 | ], 475 | 'Template:Template' => [ 476 | 'transclusions' => [1,0,1], 477 | 'redirects' => 2, 478 | ] 479 | ] 480 | ], 481 | 'file links to two redirects' => [ 482 | [ 483 | 'redirects+pagelinks' => [ 484 | [6, 'Redirect.png', 6, 'Image.png'], 485 | [6, 'Another_redirect.png', 6, 'Image.png'] 486 | ], 487 | 'imagelinks' => [ 488 | [0, 'Link', 'Redirect.png'], 489 | [0, 'Link', 'Another_redirect.png'], 490 | // Count file link to redirect as a file link to the redirect target 491 | [0, 'Link', 'Image.png'], 492 | ], 493 | ], 494 | [ 495 | 'File:Redirect.png' => [ 496 | 'filelinks' => [1,1,0] 497 | ], 498 | 'File:Another_redirect.png' => [ 499 | 'filelinks' => [1,1,0] 500 | ], 501 | 'File:Image.png' => [ 502 | 'filelinks' => [1,0,1], 503 | 'redirects' => 2, 504 | ] 505 | ] 506 | ], 507 | 508 | 'wikilinks to redirect and target' => [ 509 | [ 510 | 'redirects+pagelinks' => [ 511 | [0, 'Redirect', 0, 'Page'] 512 | ], 513 | 'pagelinks' => [ 514 | [0, 'Link', 0, 'Redirect'], 515 | [0, 'Link', 0, 'Page'] 516 | ] 517 | ], 518 | [ 519 | 'Redirect' => [ 520 | 'wikilinks' => [1,1,0] 521 | ], 522 | 'Page' => [ 523 | 'wikilinks' => [1,1,1], 524 | 'redirects' => 1, 525 | ] 526 | ] 527 | ], 528 | 'category links to redirect and target' => [ 529 | [ 530 | 'redirects+pagelinks' => [ 531 | [14, 'Redirect', 14, 'Category'] 532 | ], 533 | 'categorylinks' => [ 534 | [0, 'Link', 'Redirect'], 535 | [0, 'Link', 'Category'] 536 | ] 537 | ], 538 | [ 539 | 'Category:Redirect' => [ 540 | 'categorylinks' => [1,1,0] 541 | ], 542 | 'Category:Category' => [ 543 | 'categorylinks' => [1,1,1], 544 | 'redirects' => 1, 545 | ] 546 | ] 547 | ], 548 | 'transclusions to redirect and target' => [ 549 | [ 550 | 'redirects+pagelinks' => [ 551 | [10, 'Redirect', 10, 'Template'] 552 | ], 553 | 'templatelinks' => [ 554 | [0, 'Link', 10, 'Redirect'], 555 | [0, 'Link', 10, 'Template'] 556 | // No way to distinguish from transclusion of a redirect and transclusions of a redirect and it's target 557 | ] 558 | ], 559 | [ 560 | 'Template:Redirect' => [ 561 | 'transclusions' => [1,1,0] 562 | ], 563 | 'Template:Template' => [ 564 | 'transclusions' => [1,0,1], 565 | 'redirects' => 1, 566 | ] 567 | ] 568 | ], 569 | 'file links to redirect and target' => [ 570 | [ 571 | 'redirects+pagelinks' => [ 572 | [6, 'Redirect.png', 6, 'Image.png'] 573 | ], 574 | 'imagelinks' => [ 575 | [0, 'Link', 'Redirect.png'], 576 | [0, 'Link', 'Image.png'] 577 | // No way to distinguish from file link to a redirect and file links to a redirect and it's target 578 | ] 579 | ], 580 | [ 581 | 'File:Redirect.png' => [ 582 | 'filelinks' => [1,1,0] 583 | ], 584 | 'File:Image.png' => [ 585 | 'filelinks' => [1,0,1], 586 | 'redirects' => 1, 587 | ] 588 | ] 589 | ], 590 | 591 | 'wikilinks to pages with same title in different namespaces' => [ 592 | [ 593 | 'pagelinks' => [ 594 | [0, 'Link 1', 0, 'Page'], 595 | [0, 'Link 2', 1, 'Page'] 596 | ] 597 | ], 598 | [ 599 | 'Page' => [ 600 | 'wikilinks' => [1,1,0] 601 | ], 602 | 'Talk:Page' => [ 603 | 'wikilinks' => [1,1,0] 604 | ] 605 | ] 606 | ], 607 | 'transclusion of pages with same title in different namespaces' => [ 608 | [ 609 | 'templatelinks' => [ 610 | [0, 'Link 1', 10, 'Template'], 611 | [0, 'Link 2', 0, 'Template'] 612 | ], 613 | ], 614 | [ 615 | 'Template:Template' => [ 616 | 'transclusions' => [1,1,0] 617 | ], 618 | 'Template' => [ 619 | 'transclusions' => [1,1,0] 620 | ] 621 | ] 622 | ], 623 | 624 | 'namespace conditions for wikilink' => [ 625 | [ 626 | 'pagelinks' => [ 627 | [0, 'Link', 0, 'Page'], 628 | [1, 'Link', 0, 'Page'], 629 | [2, 'Link', 0, 'Page'] 630 | ] 631 | ], 632 | [], 633 | [ 634 | '' => [ 635 | 'Page' => [ 636 | 'wikilinks' => [3,3,0] 637 | ] 638 | ], 639 | '0,1' => [ 640 | 'Page' => [ 641 | 'wikilinks' => [2,2,0] 642 | ] 643 | ], 644 | '0' => [ 645 | 'Page' => [ 646 | 'wikilinks' => [1,1,0] 647 | ] 648 | ] 649 | ] 650 | ], 651 | 'namespace conditions for category links' => [ 652 | [ 653 | 'categorylinks' => [ 654 | [0, 'Link', 'Category'], 655 | [1, 'Link', 'Category'], 656 | [2, 'Link', 'Category'] 657 | ] 658 | ], 659 | [], 660 | [ 661 | '' => [ 662 | 'Category:Category' => [ 663 | 'categorylinks' => [3,3,0] 664 | ] 665 | ], 666 | '0,1' => [ 667 | 'Category:Category' => [ 668 | 'categorylinks' => [2,2,0] 669 | ] 670 | ], 671 | '0' => [ 672 | 'Category:Category' => [ 673 | 'categorylinks' => [1,1,0] 674 | ] 675 | ] 676 | ] 677 | ], 678 | 'namespace conditions for transclusions' => [ 679 | [ 680 | 'templatelinks' => [ 681 | [0, 'Link', 10, 'Template'], 682 | [1, 'Link', 10, 'Template'], 683 | [2, 'Link', 10, 'Template'] 684 | ] 685 | ], 686 | [], 687 | [ 688 | '' => [ 689 | 'Template:Template' => [ 690 | 'transclusions' => [3,3,0] 691 | ] 692 | ], 693 | '0,1' => [ 694 | 'Template:Template' => [ 695 | 'transclusions' => [2,2,0] 696 | ] 697 | ], 698 | '0' => [ 699 | 'Template:Template' => [ 700 | 'transclusions' => [1,1,0] 701 | ] 702 | ] 703 | ] 704 | ], 705 | 'namespace conditions for file links' => [ 706 | [ 707 | 'imagelinks' => [ 708 | [0, 'Link', 'Image.png'], 709 | [1, 'Link', 'Image.png'], 710 | [2, 'Link', 'Image.png'] 711 | ] 712 | ], 713 | [], 714 | [ 715 | '' => [ 716 | 'File:Image.png' => [ 717 | 'filelinks' => [3,3,0] 718 | ] 719 | ], 720 | '0,1' => [ 721 | 'File:Image.png' => [ 722 | 'filelinks' => [2,2,0] 723 | ] 724 | ], 725 | '0' => [ 726 | 'File:Image.png' => [ 727 | 'filelinks' => [1,1,0] 728 | ] 729 | ] 730 | ] 731 | ], 732 | 733 | 'namespace conditions from indirect for wikilinks' => [ 734 | [ 735 | 'redirects+pagelinks' => [ 736 | [0, 'Redirect', 0, 'Page'], 737 | ], 738 | 'pagelinks' => [ 739 | [0, 'Link', 0, 'Redirect'], 740 | [1, 'Link', 0, 'Redirect'], 741 | [2, 'Link', 0, 'Redirect'] 742 | ] 743 | ], 744 | [], 745 | [ 746 | '' => [ 747 | 'Redirect' => [ 748 | 'wikilinks' => [3,3,0] 749 | ], 750 | 'Page' => [ 751 | 'wikilinks' => [3,0,3], 752 | 'redirects' => 1 753 | ], 754 | ], 755 | '0,1' => [ 756 | 'Redirect' => [ 757 | 'wikilinks' => [2,2,0] 758 | ], 759 | 'Page' => [ 760 | 'wikilinks' => [2,0,2], 761 | 'redirects' => 1 762 | ] 763 | ], 764 | '0' => [ 765 | 'Redirect' => [ 766 | 'wikilinks' => [1,1,0] 767 | ], 768 | 'Page' => [ 769 | 'wikilinks' => [1,0,1], 770 | 'redirects' => 1 771 | ] 772 | ] 773 | ] 774 | ], 775 | 'namespace conditions from indirect for category links' => [ 776 | [ 777 | 'redirects+pagelinks' => [ 778 | [14, 'Redirect', 14, 'Category'], 779 | ], 780 | 'categorylinks' => [ 781 | [0, 'Link', 'Redirect'], 782 | [1, 'Link', 'Redirect'], 783 | [2, 'Link', 'Redirect'] 784 | ], 785 | ], 786 | [], 787 | [ 788 | '' => [ 789 | 'Category:Redirect' => [ 790 | 'categorylinks' => [3,3,0] 791 | ], 792 | 'Category:Category' => [ 793 | 'categorylinks' => [3,0,3], 794 | 'redirects' => 1 795 | ], 796 | ], 797 | '0,1' => [ 798 | 'Category:Redirect' => [ 799 | 'categorylinks' => [2,2,0] 800 | ], 801 | 'Category:Category' => [ 802 | 'categorylinks' => [2,0,2], 803 | 'redirects' => 0 804 | ] 805 | ], 806 | '0' => [ 807 | 'Category:Redirect' => [ 808 | 'categorylinks' => [1,1,0] 809 | ], 810 | 'Category:Category' => [ 811 | 'categorylinks' => [1,0,1], 812 | 'redirects' => 0 813 | ] 814 | ] 815 | ] 816 | ], 817 | 'namespace conditions from indirect for transclusions' => [ 818 | [ 819 | 'redirects+pagelinks' => [ 820 | [10, 'Redirect', 10, 'Template'], 821 | ], 822 | 'templatelinks' => [ 823 | [0, 'Link', 10, 'Redirect'], 824 | [1, 'Link', 10, 'Redirect'], 825 | [2, 'Link', 10, 'Redirect'], 826 | // Count transclusions of redirect as transclusions of the redirect target 827 | [0, 'Link', 10, 'Template'], 828 | [1, 'Link', 10, 'Template'], 829 | [2, 'Link', 10, 'Template'], 830 | ] 831 | ], 832 | [], 833 | [ 834 | '' => [ 835 | 'Template:Redirect' => [ 836 | 'transclusions' => [3,3,0] 837 | ], 838 | 'Template:Template' => [ 839 | 'transclusions' => [3,0,3], 840 | 'redirects' => 1 841 | ], 842 | ], 843 | '0,1' => [ 844 | 'Template:Redirect' => [ 845 | 'transclusions' => [2,2,0] 846 | ], 847 | 'Template:Template' => [ 848 | 'transclusions' => [2,0,2], 849 | 'redirects' => 0 850 | ] 851 | ], 852 | '0' => [ 853 | 'Template:Redirect' => [ 854 | 'transclusions' => [1,1,0] 855 | ], 856 | 'Template:Template' => [ 857 | 'transclusions' => [1,0,1], 858 | 'redirects' => 0 859 | ] 860 | ] 861 | ] 862 | ], 863 | 'namespace conditions from indirect for transclusions' => [ 864 | [ 865 | 'redirects+pagelinks' => [ 866 | [6, 'Redirect.png', 6, 'Image.png'], 867 | ], 868 | 'imagelinks' => [ 869 | [0, 'Link', 'Redirect.png'], 870 | [1, 'Link', 'Redirect.png'], 871 | [2, 'Link', 'Redirect.png'], 872 | // Count transclusions of redirect as transclusions of the redirect target 873 | [0, 'Link', 'Image.png'], 874 | [1, 'Link', 'Image.png'], 875 | [2, 'Link', 'Image.png'], 876 | ], 877 | ], 878 | [], 879 | [ 880 | '' => [ 881 | 'File:Redirect.png' => [ 882 | 'filelinks' => [3,3,0] 883 | ], 884 | 'File:Image.png' => [ 885 | 'filelinks' => [3,0,3], 886 | 'redirects' => 1 887 | ], 888 | ], 889 | '0,1' => [ 890 | 'File:Redirect.png' => [ 891 | 'filelinks' => [2,2,0] 892 | ], 893 | 'File:Image.png' => [ 894 | 'filelinks' => [2,0,2] 895 | ] 896 | ], 897 | '0' => [ 898 | 'File:Redirect.png' => [ 899 | 'filelinks' => [1,1,0] 900 | ], 901 | 'File:Image.png' => [ 902 | 'filelinks' => [1,0,1] 903 | ] 904 | ] 905 | ] 906 | ], 907 | ]; 908 | } 909 | 910 | /** 911 | * @dataProvider provideHtmlOutput 912 | */ 913 | public function testHtmlOutput($page, $expected, $project = null) { 914 | $this->assertEquals($expected, (new LinkCount($page, $project ?? 'linkcounttest'))->getHtml()); 915 | } 916 | 917 | public function provideHtmlOutput() { 918 | return [ 919 | 'main namespace' => [ 920 | 'Page', 921 | "
    Type
    All
    DirectIndirect
    0
    " 922 | ], 923 | 'talk namespace' => [ 924 | 'Talk:Page', 925 | "
    Type
    All
    DirectIndirect
    0
    " 926 | ], 927 | 'category namespace' => [ 928 | 'Category:Category', 929 | "
    Type
    All
    DirectIndirect
    0
    " 930 | ], 931 | 'file namespace' => [ 932 | 'File:Image.png', 933 | "
    Type
    All
    DirectIndirect
    1
    " 934 | ], 935 | '? in title' => [ 936 | '?', 937 | "
    Type
    All
    DirectIndirect
    0
    " 938 | ], 939 | 'no parameters' => [ 940 | '', 941 | 'View the number of links (wikilinks, redirects, transclusions, file links, and category links) to any page on any Wikimedia project.', 942 | '' 943 | ], 944 | 'no title (error)' => [ 945 | '', 946 | '
    Page name is required.
    ' 947 | ] 948 | ]; 949 | } 950 | 951 | /** 952 | * @dataProvider provideJsonOutput 953 | */ 954 | public function testJsonOutput($page, $expected, $project = null) { 955 | $this->assertEquals($expected, (new LinkCount($page, $project ?? 'linkcounttest'))->getJson()); 956 | } 957 | 958 | public function provideJsonOutput() { 959 | return [ 960 | 'main namespace' => [ 961 | 'Page', 962 | '{"filelinks":null,"categorylinks":null,"wikilinks":{"all":0,"direct":0,"indirect":0},"redirects":0,"transclusions":{"all":0,"direct":0,"indirect":0}}' 963 | ], 964 | 'talk namespace' => [ 965 | 'Talk:Page', 966 | '{"filelinks":null,"categorylinks":null,"wikilinks":{"all":0,"direct":0,"indirect":0},"redirects":0,"transclusions":{"all":0,"direct":0,"indirect":0}}' 967 | ], 968 | 'category namespace' => [ 969 | 'Category:Category', 970 | '{"filelinks":null,"categorylinks":{"all":0,"direct":0,"indirect":0},"wikilinks":{"all":0,"direct":0,"indirect":0},"redirects":0,"transclusions":{"all":0,"direct":0,"indirect":0}}' 971 | ], 972 | 'file namespace' => [ 973 | 'File:Image.png', 974 | '{"filelinks":{"all":3,"direct":0,"indirect":3},"categorylinks":null,"wikilinks":{"all":0,"direct":0,"indirect":0},"redirects":1,"transclusions":{"all":0,"direct":0,"indirect":0}}' 975 | ], 976 | 'no parameters' => [ 977 | '', 978 | '{}', 979 | '' 980 | ], 981 | 'no title (error)' => [ 982 | '', 983 | '{"error":"Page name is required."}' 984 | ] 985 | ]; 986 | } 987 | } 988 | -------------------------------------------------------------------------------- /tests/TestDatabaseFactory.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedNamespace, $title->getNamespaceId()); 13 | $this->assertEquals($expectedDatabaseKey, $title->getDBKey()); 14 | } 15 | 16 | public function provideTitle() { 17 | return [ 18 | 'main namespace' => [ 19 | 'Foo', 20 | 0, 21 | 'Foo' 22 | ], 23 | 'main namespace with colon' => [ 24 | ':Foo', 25 | 0, 26 | 'Foo' 27 | ], 28 | 'talk namespace' => [ 29 | 'Talk:Foo', 30 | 1, 31 | 'Foo' 32 | ], 33 | 'namespace with space' => [ 34 | 'Template talk:Foo', 35 | 11, 36 | 'Foo' 37 | ], 38 | 'namespace with underscores' => [ 39 | 'Template_talk:Foo', 40 | 11, 41 | 'Foo' 42 | ], 43 | 'page with spaces' => [ 44 | 'Foo bar baz', 45 | 0, 46 | 'Foo_bar_baz' 47 | ], 48 | 'page with underscores' => [ 49 | 'Foo_bar_baz', 50 | 0, 51 | 'Foo_bar_baz' 52 | ], 53 | 'lowercase first letter' => [ 54 | 'foo bar baz', 55 | 0, 56 | 'Foo_bar_baz' 57 | ], 58 | /* 59 | // There are no active case-sensitive namespaces 60 | 'case-sensitive namespace' => [ 61 | 'Gadget definition talk:foo', 62 | 2303, 63 | 'foo' 64 | ], 65 | */ 66 | 'alias namespace' => [ 67 | 'Image:Foo.png', 68 | 6, 69 | 'Foo.png' 70 | ], 71 | 'template namespace with colon' => [ 72 | ':Template:Foo', 73 | 10, 74 | 'Foo' 75 | ], 76 | 'test invalid namespace' => [ 77 | 'Foo:Bar', 78 | 0, 79 | 'Foo:Bar' 80 | ], 81 | 'test invalid namespace with prefix colon' => [ 82 | ':Foo:Bar', 83 | 0, 84 | 'Foo:Bar' 85 | ], 86 | 'test lowercase invalid namespace' => [ 87 | 'foo:bar', 88 | 0, 89 | 'Foo:bar' 90 | ], 91 | 'test canonical name' => [ 92 | 'Project:Foo', 93 | 4, 94 | 'Foo' 95 | ], 96 | 'test wiki defined name' => [ 97 | 'Wikipedia:Foo', 98 | 4, 99 | 'Foo' 100 | ] 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/createdb.php: -------------------------------------------------------------------------------- 1 | exec(file_get_contents(__DIR__ . '/createdb.sql')); 8 | 9 | echo "Created database linkcounttest.\n"; 10 | -------------------------------------------------------------------------------- /tests/createdb.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS linkcounttest; 2 | CREATE DATABASE linkcounttest; 3 | USE linkcounttest; 4 | 5 | CREATE TABLE wiki ( 6 | dbname varbinary(255) NOT NULL DEFAULT '', 7 | url varbinary(255) NOT NULL DEFAULT '' 8 | ); 9 | 10 | INSERT INTO wiki (dbname, url) VALUES 11 | ('linkcounttest', 'https://en.wikipedia.org'), 12 | -- Included for testing on autocomplete of the project input 13 | ('eewiki', 'https://ee.wikipedia.org'), 14 | ('elwiki', 'https://el.wikipedia.org'), 15 | ('elwikibooks', 'https://el.wikibooks.org'), 16 | ('elwikinews', 'https://el.wikinews.org'), 17 | ('elwikiquote', 'https://el.wikiquote.org'), 18 | ('elwikisource', 'https://el.wikisource.org'), 19 | ('elwikiversity', 'https://el.wikiversity.org'), 20 | ('elwikivoyage', 'https://el.wikivoyage.org'), 21 | ('elwiktionary', 'https://el.wiktionary.org'), 22 | ('emlwiki', 'https://eml.wikipedia.org'), 23 | ('enwikibooks', 'https://en.wikibooks.org'), 24 | ('enwikinews', 'https://en.wikinews.org'), 25 | ('enwikiquote', 'https://en.wikiquote.org'), 26 | ('enwikisource', 'https://en.wikisource.org'), 27 | ('enwikiversity', 'https://en.wikiversity.org'), 28 | ('enwikivoyage', 'https://en.wikivoyage.org'), 29 | ('enwiktionary', 'https://en.wiktionary.org'), 30 | ('eowiki', 'https://eo.wikipedia.org'), 31 | ('eowikibooks', 'https://eo.wikibooks.org'), 32 | ('eowikinews', 'https://eo.wikinews.org'), 33 | ('eowikiquote', 'https://eo.wikiquote.org'), 34 | ('eowikisource', 'https://eo.wikisource.org'), 35 | ('eowikivoyage', 'https://eo.wikivoyage.org'), 36 | ('eowiktionary', 'https://eo.wiktionary.org'), 37 | ('eswiki', 'https://es.wikipedia.org'), 38 | ('eswikibooks', 'https://es.wikibooks.org'), 39 | ('eswikinews', 'https://es.wikinews.org'), 40 | ('eswikiquote', 'https://es.wikiquote.org'), 41 | ('eswikisource', 'https://es.wikisource.org'), 42 | ('eswikiversity', 'https://es.wikiversity.org'), 43 | ('eswikivoyage', 'https://es.wikivoyage.org'), 44 | ('eswiktionary', 'https://es.wiktionary.org'), 45 | ('etwiki', 'https://et.wikipedia.org'), 46 | ('etwikibooks', 'https://et.wikibooks.org'), 47 | ('etwikimedia', 'https://ee.wikimedia.org'), 48 | ('etwikiquote', 'https://et.wikiquote.org'), 49 | ('etwikisource', 'https://et.wikisource.org'), 50 | ('etwiktionary', 'https://et.wiktionary.org'), 51 | ('euwiki', 'https://eu.wikipedia.org'), 52 | ('euwikibooks', 'https://eu.wikibooks.org'), 53 | ('euwikiquote', 'https://eu.wikiquote.org'), 54 | ('euwikisource', 'https://eu.wikisource.org'), 55 | ('euwiktionary', 'https://eu.wiktionary.org'); 56 | 57 | CREATE TABLE categorylinks ( 58 | cl_from int(10) unsigned NOT NULL DEFAULT 0, 59 | cl_to varbinary(255) NOT NULL DEFAULT '' 60 | ); 61 | 62 | CREATE TABLE imagelinks ( 63 | il_from int(10) unsigned NOT NULL DEFAULT 0, 64 | il_to varbinary(255) NOT NULL DEFAULT '', 65 | il_from_namespace int(11) NOT NULL DEFAULT 0 66 | ); 67 | 68 | CREATE TABLE page ( 69 | page_id int(10) unsigned NOT NULL, 70 | page_namespace int(11) NOT NULL, 71 | page_title varbinary(255) NOT NULL 72 | ); 73 | 74 | CREATE TABLE pagelinks ( 75 | pl_from int(10) unsigned NOT NULL DEFAULT 0, 76 | pl_from_namespace int(11) NOT NULL DEFAULT 0, 77 | pl_target_id bigint(20) unsigned DEFAULT 0 78 | ); 79 | 80 | CREATE TABLE redirect ( 81 | rd_from int(10) unsigned NOT NULL DEFAULT 0, 82 | rd_namespace int(11) NOT NULL DEFAULT 0, 83 | rd_title varbinary(255) NOT NULL DEFAULT '', 84 | rd_interwiki varbinary(32) DEFAULT NULL 85 | ); 86 | 87 | CREATE TABLE templatelinks ( 88 | tl_from int(10) unsigned NOT NULL DEFAULT 0, 89 | tl_from_namespace int(11) NOT NULL DEFAULT 0, 90 | tl_target_id bigint(20) unsigned DEFAULT 0 91 | ); 92 | 93 | CREATE TABLE linktarget ( 94 | lt_id bigint(20) unsigned NOT NULL DEFAULT 0, 95 | lt_namespace int(11) NOT NULL DEFAULT 0, 96 | lt_title varbinary(255) NOT NULL DEFAULT '' 97 | ); 98 | --------------------------------------------------------------------------------