├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── composer.lock ├── conf └── apache-extra.conf ├── docker-compose.yml ├── htdocs ├── .htaccess ├── bookmarkManager.js ├── bookmarklet.php ├── index.php ├── normalize.css └── style.css ├── htpasswd ├── phpunit.xml ├── psalm.xml ├── src ├── BookmarkManager.php └── DB.php └── tests ├── BookmarkManagerTest.php ├── bootstrap.php ├── extractTitle.html └── extractTitle.latin1.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_size = 2 9 | 10 | [*.{js,css}] 11 | indent_size = 2 12 | 13 | [*.php] 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .env 3 | .phpunit.result.cache 4 | .php-cs-fixer.cache 5 | /db 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-apache 2 | 3 | RUN apt-get update -yq && \ 4 | apt-get install -yq libzip-dev 5 | 6 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 7 | 8 | RUN a2enmod rewrite 9 | RUN docker-php-ext-install zip 10 | 11 | WORKDIR /app 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2021 Sebastian Volland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default up bg down test clean bash 2 | 3 | DOCKER_COMPOSE = USERID=$(shell id -u) GID=$(shell id -g) docker compose --env-file=.env 4 | DOCKER_COMPOSE_RUN = $(DOCKER_COMPOSE) run --rm --user $(shell id -u):$(shell id -g) app 5 | DOCKER_COMPOSE_UP = $(DOCKER_COMPOSE) up --force-recreate --build 6 | 7 | default: bg 8 | 9 | up: .env vendor/autoload.php 10 | $(DOCKER_COMPOSE_UP) 11 | 12 | bg: .env vendor/autoload.php 13 | $(DOCKER_COMPOSE_UP) -d 14 | 15 | down: 16 | $(DOCKER_COMPOSE) down 17 | 18 | test: vendor/bin/phpunit 19 | vendor/bin/phpunit . 20 | 21 | clean: down 22 | $(DOCKER_COMPOSE) rm 23 | 24 | bash: 25 | docker exec -it --user root b_app_1 bash 26 | 27 | .env: 28 | touch .env 29 | 30 | vendor/autoload.php: 31 | $(DOCKER_COMPOSE_RUN) composer install --no-dev --no-cache 32 | 33 | vendor/bin/phpunit: 34 | $(DOCKER_COMPOSE_RUN) composer install --dev --no-cache 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # b - Bookmark manager 2 | 3 | b is a minimalistic bookmark manager for your own server. Written in PHP. 4 | Bookmarks are stored in a sqlite database. Features: 5 | 6 | * filtering 7 | * tagging 8 | * automatic fetching of page title 9 | * infinite scrolling (optional) 10 | * bookmarklet 11 | * multiple users 12 | 13 | ### Requirements 14 | 15 | * make 16 | * docker + docker compose 17 | 18 | Tested on Ubuntu 21.04 and macOS Big Sur. 19 | 20 | ### Setup 21 | 22 | This web app uses HTTP basic auth password protection. Create a `htusers` file 23 | and specify username/password like this: 24 | 25 | mkdir db 26 | ./htpasswd -c db/htusers peter 27 | 28 | The bookmark manager can host multiple databases. To initialize a new database, 29 | simply create a subdirectory: 30 | 31 | mkdir db/peter 32 | 33 | This will make bookmarks accessible via `http://localhost:9090/peter`. 34 | 35 | Use `make` to start the webserver container and `make down` to stop it. 36 | 37 | To prevent forking the container into the background, use `make up` instead of 38 | `make` (useful for debugging). 39 | 40 | ### How to use 41 | 42 | * To add a new bookmark, simply paste it into the input field and press 43 | return. the url may be followed by hash tags, e.g. `http://example.com 44 | #example #bla #wurst` 45 | * The website's title is automatically fetched and the bookmark is added to 46 | the database. 47 | * Edit title by double clicking it. This opens a prompt-dialog where you can 48 | edit the title. Enter '-' (minus sign) to remove an entry. 49 | * To edit the URL, double click beside the link. 50 | * The input field can also be used to filter bookmarks. Filtering is done with 51 | a full-text search on all titles. Search terms are separated by spaces 52 | and joined with AND. 53 | 54 | ### Infinite scrolling 55 | 56 | If you have a massive amount of bookmarks and you don't want to load them all at 57 | once, you can activate infinite scrolling. This will load a limited amount of 58 | bookmarks initially and load more when you scroll to the bottom of the page. 59 | Activate infinite scrolling by adding `INFINITE_SCROLLING=200` to `.env`. 60 | Replace `200` with the number of bookmarks you want to load each time you hit 61 | the bottom. 62 | 63 | ### Bookmarklet 64 | 65 | Visit `/[user]/bookmarklet` to access the user's bookmarklet, e.g. 66 | `http://bookmarks.example.com/peter/bookmarklet`. (Thanks to nibreh for the 67 | suggestion!) 68 | 69 | ### Credits 70 | 71 | Copyright (c) 2011-2021 Sebastian Volland http://github.com/sebcode 72 | 73 | The source code is licensed under the terms of the MIT license (see LICENSE 74 | file). 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sebcode/b", 3 | "description": "Bookmark manager", 4 | "autoload": { 5 | "psr-4": { 6 | "B\\": "src/" 7 | } 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^9" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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": "b5be26ef95e4597fa340e6977bdd2e1e", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.4.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", 21 | "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^9", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^0.16 || ^1", 32 | "phpstan/phpstan": "^1.4", 33 | "phpstan/phpstan-phpunit": "^1", 34 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 35 | "vimeo/psalm": "^4.22" 36 | }, 37 | "type": "library", 38 | "autoload": { 39 | "psr-4": { 40 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Marco Pivetta", 50 | "email": "ocramius@gmail.com", 51 | "homepage": "https://ocramius.github.io/" 52 | } 53 | ], 54 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 55 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 56 | "keywords": [ 57 | "constructor", 58 | "instantiate" 59 | ], 60 | "support": { 61 | "issues": "https://github.com/doctrine/instantiator/issues", 62 | "source": "https://github.com/doctrine/instantiator/tree/1.4.1" 63 | }, 64 | "funding": [ 65 | { 66 | "url": "https://www.doctrine-project.org/sponsorship.html", 67 | "type": "custom" 68 | }, 69 | { 70 | "url": "https://www.patreon.com/phpdoctrine", 71 | "type": "patreon" 72 | }, 73 | { 74 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 75 | "type": "tidelift" 76 | } 77 | ], 78 | "time": "2022-03-03T08:28:38+00:00" 79 | }, 80 | { 81 | "name": "myclabs/deep-copy", 82 | "version": "1.11.0", 83 | "source": { 84 | "type": "git", 85 | "url": "https://github.com/myclabs/DeepCopy.git", 86 | "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" 87 | }, 88 | "dist": { 89 | "type": "zip", 90 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", 91 | "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", 92 | "shasum": "" 93 | }, 94 | "require": { 95 | "php": "^7.1 || ^8.0" 96 | }, 97 | "conflict": { 98 | "doctrine/collections": "<1.6.8", 99 | "doctrine/common": "<2.13.3 || >=3,<3.2.2" 100 | }, 101 | "require-dev": { 102 | "doctrine/collections": "^1.6.8", 103 | "doctrine/common": "^2.13.3 || ^3.2.2", 104 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 105 | }, 106 | "type": "library", 107 | "autoload": { 108 | "files": [ 109 | "src/DeepCopy/deep_copy.php" 110 | ], 111 | "psr-4": { 112 | "DeepCopy\\": "src/DeepCopy/" 113 | } 114 | }, 115 | "notification-url": "https://packagist.org/downloads/", 116 | "license": [ 117 | "MIT" 118 | ], 119 | "description": "Create deep copies (clones) of your objects", 120 | "keywords": [ 121 | "clone", 122 | "copy", 123 | "duplicate", 124 | "object", 125 | "object graph" 126 | ], 127 | "support": { 128 | "issues": "https://github.com/myclabs/DeepCopy/issues", 129 | "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" 130 | }, 131 | "funding": [ 132 | { 133 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 134 | "type": "tidelift" 135 | } 136 | ], 137 | "time": "2022-03-03T13:19:32+00:00" 138 | }, 139 | { 140 | "name": "nikic/php-parser", 141 | "version": "v4.15.2", 142 | "source": { 143 | "type": "git", 144 | "url": "https://github.com/nikic/PHP-Parser.git", 145 | "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" 146 | }, 147 | "dist": { 148 | "type": "zip", 149 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", 150 | "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", 151 | "shasum": "" 152 | }, 153 | "require": { 154 | "ext-tokenizer": "*", 155 | "php": ">=7.0" 156 | }, 157 | "require-dev": { 158 | "ircmaxell/php-yacc": "^0.0.7", 159 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" 160 | }, 161 | "bin": [ 162 | "bin/php-parse" 163 | ], 164 | "type": "library", 165 | "extra": { 166 | "branch-alias": { 167 | "dev-master": "4.9-dev" 168 | } 169 | }, 170 | "autoload": { 171 | "psr-4": { 172 | "PhpParser\\": "lib/PhpParser" 173 | } 174 | }, 175 | "notification-url": "https://packagist.org/downloads/", 176 | "license": [ 177 | "BSD-3-Clause" 178 | ], 179 | "authors": [ 180 | { 181 | "name": "Nikita Popov" 182 | } 183 | ], 184 | "description": "A PHP parser written in PHP", 185 | "keywords": [ 186 | "parser", 187 | "php" 188 | ], 189 | "support": { 190 | "issues": "https://github.com/nikic/PHP-Parser/issues", 191 | "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" 192 | }, 193 | "time": "2022-11-12T15:38:23+00:00" 194 | }, 195 | { 196 | "name": "phar-io/manifest", 197 | "version": "2.0.3", 198 | "source": { 199 | "type": "git", 200 | "url": "https://github.com/phar-io/manifest.git", 201 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53" 202 | }, 203 | "dist": { 204 | "type": "zip", 205 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", 206 | "reference": "97803eca37d319dfa7826cc2437fc020857acb53", 207 | "shasum": "" 208 | }, 209 | "require": { 210 | "ext-dom": "*", 211 | "ext-phar": "*", 212 | "ext-xmlwriter": "*", 213 | "phar-io/version": "^3.0.1", 214 | "php": "^7.2 || ^8.0" 215 | }, 216 | "type": "library", 217 | "extra": { 218 | "branch-alias": { 219 | "dev-master": "2.0.x-dev" 220 | } 221 | }, 222 | "autoload": { 223 | "classmap": [ 224 | "src/" 225 | ] 226 | }, 227 | "notification-url": "https://packagist.org/downloads/", 228 | "license": [ 229 | "BSD-3-Clause" 230 | ], 231 | "authors": [ 232 | { 233 | "name": "Arne Blankerts", 234 | "email": "arne@blankerts.de", 235 | "role": "Developer" 236 | }, 237 | { 238 | "name": "Sebastian Heuer", 239 | "email": "sebastian@phpeople.de", 240 | "role": "Developer" 241 | }, 242 | { 243 | "name": "Sebastian Bergmann", 244 | "email": "sebastian@phpunit.de", 245 | "role": "Developer" 246 | } 247 | ], 248 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 249 | "support": { 250 | "issues": "https://github.com/phar-io/manifest/issues", 251 | "source": "https://github.com/phar-io/manifest/tree/2.0.3" 252 | }, 253 | "time": "2021-07-20T11:28:43+00:00" 254 | }, 255 | { 256 | "name": "phar-io/version", 257 | "version": "3.2.1", 258 | "source": { 259 | "type": "git", 260 | "url": "https://github.com/phar-io/version.git", 261 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 262 | }, 263 | "dist": { 264 | "type": "zip", 265 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 266 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 267 | "shasum": "" 268 | }, 269 | "require": { 270 | "php": "^7.2 || ^8.0" 271 | }, 272 | "type": "library", 273 | "autoload": { 274 | "classmap": [ 275 | "src/" 276 | ] 277 | }, 278 | "notification-url": "https://packagist.org/downloads/", 279 | "license": [ 280 | "BSD-3-Clause" 281 | ], 282 | "authors": [ 283 | { 284 | "name": "Arne Blankerts", 285 | "email": "arne@blankerts.de", 286 | "role": "Developer" 287 | }, 288 | { 289 | "name": "Sebastian Heuer", 290 | "email": "sebastian@phpeople.de", 291 | "role": "Developer" 292 | }, 293 | { 294 | "name": "Sebastian Bergmann", 295 | "email": "sebastian@phpunit.de", 296 | "role": "Developer" 297 | } 298 | ], 299 | "description": "Library for handling version information and constraints", 300 | "support": { 301 | "issues": "https://github.com/phar-io/version/issues", 302 | "source": "https://github.com/phar-io/version/tree/3.2.1" 303 | }, 304 | "time": "2022-02-21T01:04:05+00:00" 305 | }, 306 | { 307 | "name": "phpunit/php-code-coverage", 308 | "version": "9.2.23", 309 | "source": { 310 | "type": "git", 311 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 312 | "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" 313 | }, 314 | "dist": { 315 | "type": "zip", 316 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", 317 | "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", 318 | "shasum": "" 319 | }, 320 | "require": { 321 | "ext-dom": "*", 322 | "ext-libxml": "*", 323 | "ext-xmlwriter": "*", 324 | "nikic/php-parser": "^4.14", 325 | "php": ">=7.3", 326 | "phpunit/php-file-iterator": "^3.0.3", 327 | "phpunit/php-text-template": "^2.0.2", 328 | "sebastian/code-unit-reverse-lookup": "^2.0.2", 329 | "sebastian/complexity": "^2.0", 330 | "sebastian/environment": "^5.1.2", 331 | "sebastian/lines-of-code": "^1.0.3", 332 | "sebastian/version": "^3.0.1", 333 | "theseer/tokenizer": "^1.2.0" 334 | }, 335 | "require-dev": { 336 | "phpunit/phpunit": "^9.3" 337 | }, 338 | "suggest": { 339 | "ext-pcov": "*", 340 | "ext-xdebug": "*" 341 | }, 342 | "type": "library", 343 | "extra": { 344 | "branch-alias": { 345 | "dev-master": "9.2-dev" 346 | } 347 | }, 348 | "autoload": { 349 | "classmap": [ 350 | "src/" 351 | ] 352 | }, 353 | "notification-url": "https://packagist.org/downloads/", 354 | "license": [ 355 | "BSD-3-Clause" 356 | ], 357 | "authors": [ 358 | { 359 | "name": "Sebastian Bergmann", 360 | "email": "sebastian@phpunit.de", 361 | "role": "lead" 362 | } 363 | ], 364 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 365 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 366 | "keywords": [ 367 | "coverage", 368 | "testing", 369 | "xunit" 370 | ], 371 | "support": { 372 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 373 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" 374 | }, 375 | "funding": [ 376 | { 377 | "url": "https://github.com/sebastianbergmann", 378 | "type": "github" 379 | } 380 | ], 381 | "time": "2022-12-28T12:41:10+00:00" 382 | }, 383 | { 384 | "name": "phpunit/php-file-iterator", 385 | "version": "3.0.6", 386 | "source": { 387 | "type": "git", 388 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 389 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" 390 | }, 391 | "dist": { 392 | "type": "zip", 393 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 394 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 395 | "shasum": "" 396 | }, 397 | "require": { 398 | "php": ">=7.3" 399 | }, 400 | "require-dev": { 401 | "phpunit/phpunit": "^9.3" 402 | }, 403 | "type": "library", 404 | "extra": { 405 | "branch-alias": { 406 | "dev-master": "3.0-dev" 407 | } 408 | }, 409 | "autoload": { 410 | "classmap": [ 411 | "src/" 412 | ] 413 | }, 414 | "notification-url": "https://packagist.org/downloads/", 415 | "license": [ 416 | "BSD-3-Clause" 417 | ], 418 | "authors": [ 419 | { 420 | "name": "Sebastian Bergmann", 421 | "email": "sebastian@phpunit.de", 422 | "role": "lead" 423 | } 424 | ], 425 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 426 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 427 | "keywords": [ 428 | "filesystem", 429 | "iterator" 430 | ], 431 | "support": { 432 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 433 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" 434 | }, 435 | "funding": [ 436 | { 437 | "url": "https://github.com/sebastianbergmann", 438 | "type": "github" 439 | } 440 | ], 441 | "time": "2021-12-02T12:48:52+00:00" 442 | }, 443 | { 444 | "name": "phpunit/php-invoker", 445 | "version": "3.1.1", 446 | "source": { 447 | "type": "git", 448 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 449 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" 450 | }, 451 | "dist": { 452 | "type": "zip", 453 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 454 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 455 | "shasum": "" 456 | }, 457 | "require": { 458 | "php": ">=7.3" 459 | }, 460 | "require-dev": { 461 | "ext-pcntl": "*", 462 | "phpunit/phpunit": "^9.3" 463 | }, 464 | "suggest": { 465 | "ext-pcntl": "*" 466 | }, 467 | "type": "library", 468 | "extra": { 469 | "branch-alias": { 470 | "dev-master": "3.1-dev" 471 | } 472 | }, 473 | "autoload": { 474 | "classmap": [ 475 | "src/" 476 | ] 477 | }, 478 | "notification-url": "https://packagist.org/downloads/", 479 | "license": [ 480 | "BSD-3-Clause" 481 | ], 482 | "authors": [ 483 | { 484 | "name": "Sebastian Bergmann", 485 | "email": "sebastian@phpunit.de", 486 | "role": "lead" 487 | } 488 | ], 489 | "description": "Invoke callables with a timeout", 490 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 491 | "keywords": [ 492 | "process" 493 | ], 494 | "support": { 495 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 496 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" 497 | }, 498 | "funding": [ 499 | { 500 | "url": "https://github.com/sebastianbergmann", 501 | "type": "github" 502 | } 503 | ], 504 | "time": "2020-09-28T05:58:55+00:00" 505 | }, 506 | { 507 | "name": "phpunit/php-text-template", 508 | "version": "2.0.4", 509 | "source": { 510 | "type": "git", 511 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 512 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" 513 | }, 514 | "dist": { 515 | "type": "zip", 516 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 517 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 518 | "shasum": "" 519 | }, 520 | "require": { 521 | "php": ">=7.3" 522 | }, 523 | "require-dev": { 524 | "phpunit/phpunit": "^9.3" 525 | }, 526 | "type": "library", 527 | "extra": { 528 | "branch-alias": { 529 | "dev-master": "2.0-dev" 530 | } 531 | }, 532 | "autoload": { 533 | "classmap": [ 534 | "src/" 535 | ] 536 | }, 537 | "notification-url": "https://packagist.org/downloads/", 538 | "license": [ 539 | "BSD-3-Clause" 540 | ], 541 | "authors": [ 542 | { 543 | "name": "Sebastian Bergmann", 544 | "email": "sebastian@phpunit.de", 545 | "role": "lead" 546 | } 547 | ], 548 | "description": "Simple template engine.", 549 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 550 | "keywords": [ 551 | "template" 552 | ], 553 | "support": { 554 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 555 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" 556 | }, 557 | "funding": [ 558 | { 559 | "url": "https://github.com/sebastianbergmann", 560 | "type": "github" 561 | } 562 | ], 563 | "time": "2020-10-26T05:33:50+00:00" 564 | }, 565 | { 566 | "name": "phpunit/php-timer", 567 | "version": "5.0.3", 568 | "source": { 569 | "type": "git", 570 | "url": "https://github.com/sebastianbergmann/php-timer.git", 571 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" 572 | }, 573 | "dist": { 574 | "type": "zip", 575 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 576 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 577 | "shasum": "" 578 | }, 579 | "require": { 580 | "php": ">=7.3" 581 | }, 582 | "require-dev": { 583 | "phpunit/phpunit": "^9.3" 584 | }, 585 | "type": "library", 586 | "extra": { 587 | "branch-alias": { 588 | "dev-master": "5.0-dev" 589 | } 590 | }, 591 | "autoload": { 592 | "classmap": [ 593 | "src/" 594 | ] 595 | }, 596 | "notification-url": "https://packagist.org/downloads/", 597 | "license": [ 598 | "BSD-3-Clause" 599 | ], 600 | "authors": [ 601 | { 602 | "name": "Sebastian Bergmann", 603 | "email": "sebastian@phpunit.de", 604 | "role": "lead" 605 | } 606 | ], 607 | "description": "Utility class for timing", 608 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 609 | "keywords": [ 610 | "timer" 611 | ], 612 | "support": { 613 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 614 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" 615 | }, 616 | "funding": [ 617 | { 618 | "url": "https://github.com/sebastianbergmann", 619 | "type": "github" 620 | } 621 | ], 622 | "time": "2020-10-26T13:16:10+00:00" 623 | }, 624 | { 625 | "name": "phpunit/phpunit", 626 | "version": "9.5.27", 627 | "source": { 628 | "type": "git", 629 | "url": "https://github.com/sebastianbergmann/phpunit.git", 630 | "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38" 631 | }, 632 | "dist": { 633 | "type": "zip", 634 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38", 635 | "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38", 636 | "shasum": "" 637 | }, 638 | "require": { 639 | "doctrine/instantiator": "^1.3.1", 640 | "ext-dom": "*", 641 | "ext-json": "*", 642 | "ext-libxml": "*", 643 | "ext-mbstring": "*", 644 | "ext-xml": "*", 645 | "ext-xmlwriter": "*", 646 | "myclabs/deep-copy": "^1.10.1", 647 | "phar-io/manifest": "^2.0.3", 648 | "phar-io/version": "^3.0.2", 649 | "php": ">=7.3", 650 | "phpunit/php-code-coverage": "^9.2.13", 651 | "phpunit/php-file-iterator": "^3.0.5", 652 | "phpunit/php-invoker": "^3.1.1", 653 | "phpunit/php-text-template": "^2.0.3", 654 | "phpunit/php-timer": "^5.0.2", 655 | "sebastian/cli-parser": "^1.0.1", 656 | "sebastian/code-unit": "^1.0.6", 657 | "sebastian/comparator": "^4.0.8", 658 | "sebastian/diff": "^4.0.3", 659 | "sebastian/environment": "^5.1.3", 660 | "sebastian/exporter": "^4.0.5", 661 | "sebastian/global-state": "^5.0.1", 662 | "sebastian/object-enumerator": "^4.0.3", 663 | "sebastian/resource-operations": "^3.0.3", 664 | "sebastian/type": "^3.2", 665 | "sebastian/version": "^3.0.2" 666 | }, 667 | "suggest": { 668 | "ext-soap": "*", 669 | "ext-xdebug": "*" 670 | }, 671 | "bin": [ 672 | "phpunit" 673 | ], 674 | "type": "library", 675 | "extra": { 676 | "branch-alias": { 677 | "dev-master": "9.5-dev" 678 | } 679 | }, 680 | "autoload": { 681 | "files": [ 682 | "src/Framework/Assert/Functions.php" 683 | ], 684 | "classmap": [ 685 | "src/" 686 | ] 687 | }, 688 | "notification-url": "https://packagist.org/downloads/", 689 | "license": [ 690 | "BSD-3-Clause" 691 | ], 692 | "authors": [ 693 | { 694 | "name": "Sebastian Bergmann", 695 | "email": "sebastian@phpunit.de", 696 | "role": "lead" 697 | } 698 | ], 699 | "description": "The PHP Unit Testing framework.", 700 | "homepage": "https://phpunit.de/", 701 | "keywords": [ 702 | "phpunit", 703 | "testing", 704 | "xunit" 705 | ], 706 | "support": { 707 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 708 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27" 709 | }, 710 | "funding": [ 711 | { 712 | "url": "https://phpunit.de/sponsors.html", 713 | "type": "custom" 714 | }, 715 | { 716 | "url": "https://github.com/sebastianbergmann", 717 | "type": "github" 718 | }, 719 | { 720 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 721 | "type": "tidelift" 722 | } 723 | ], 724 | "time": "2022-12-09T07:31:23+00:00" 725 | }, 726 | { 727 | "name": "sebastian/cli-parser", 728 | "version": "1.0.1", 729 | "source": { 730 | "type": "git", 731 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 732 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" 733 | }, 734 | "dist": { 735 | "type": "zip", 736 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", 737 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", 738 | "shasum": "" 739 | }, 740 | "require": { 741 | "php": ">=7.3" 742 | }, 743 | "require-dev": { 744 | "phpunit/phpunit": "^9.3" 745 | }, 746 | "type": "library", 747 | "extra": { 748 | "branch-alias": { 749 | "dev-master": "1.0-dev" 750 | } 751 | }, 752 | "autoload": { 753 | "classmap": [ 754 | "src/" 755 | ] 756 | }, 757 | "notification-url": "https://packagist.org/downloads/", 758 | "license": [ 759 | "BSD-3-Clause" 760 | ], 761 | "authors": [ 762 | { 763 | "name": "Sebastian Bergmann", 764 | "email": "sebastian@phpunit.de", 765 | "role": "lead" 766 | } 767 | ], 768 | "description": "Library for parsing CLI options", 769 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 770 | "support": { 771 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 772 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" 773 | }, 774 | "funding": [ 775 | { 776 | "url": "https://github.com/sebastianbergmann", 777 | "type": "github" 778 | } 779 | ], 780 | "time": "2020-09-28T06:08:49+00:00" 781 | }, 782 | { 783 | "name": "sebastian/code-unit", 784 | "version": "1.0.8", 785 | "source": { 786 | "type": "git", 787 | "url": "https://github.com/sebastianbergmann/code-unit.git", 788 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" 789 | }, 790 | "dist": { 791 | "type": "zip", 792 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", 793 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", 794 | "shasum": "" 795 | }, 796 | "require": { 797 | "php": ">=7.3" 798 | }, 799 | "require-dev": { 800 | "phpunit/phpunit": "^9.3" 801 | }, 802 | "type": "library", 803 | "extra": { 804 | "branch-alias": { 805 | "dev-master": "1.0-dev" 806 | } 807 | }, 808 | "autoload": { 809 | "classmap": [ 810 | "src/" 811 | ] 812 | }, 813 | "notification-url": "https://packagist.org/downloads/", 814 | "license": [ 815 | "BSD-3-Clause" 816 | ], 817 | "authors": [ 818 | { 819 | "name": "Sebastian Bergmann", 820 | "email": "sebastian@phpunit.de", 821 | "role": "lead" 822 | } 823 | ], 824 | "description": "Collection of value objects that represent the PHP code units", 825 | "homepage": "https://github.com/sebastianbergmann/code-unit", 826 | "support": { 827 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 828 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" 829 | }, 830 | "funding": [ 831 | { 832 | "url": "https://github.com/sebastianbergmann", 833 | "type": "github" 834 | } 835 | ], 836 | "time": "2020-10-26T13:08:54+00:00" 837 | }, 838 | { 839 | "name": "sebastian/code-unit-reverse-lookup", 840 | "version": "2.0.3", 841 | "source": { 842 | "type": "git", 843 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 844 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" 845 | }, 846 | "dist": { 847 | "type": "zip", 848 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 849 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 850 | "shasum": "" 851 | }, 852 | "require": { 853 | "php": ">=7.3" 854 | }, 855 | "require-dev": { 856 | "phpunit/phpunit": "^9.3" 857 | }, 858 | "type": "library", 859 | "extra": { 860 | "branch-alias": { 861 | "dev-master": "2.0-dev" 862 | } 863 | }, 864 | "autoload": { 865 | "classmap": [ 866 | "src/" 867 | ] 868 | }, 869 | "notification-url": "https://packagist.org/downloads/", 870 | "license": [ 871 | "BSD-3-Clause" 872 | ], 873 | "authors": [ 874 | { 875 | "name": "Sebastian Bergmann", 876 | "email": "sebastian@phpunit.de" 877 | } 878 | ], 879 | "description": "Looks up which function or method a line of code belongs to", 880 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 881 | "support": { 882 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 883 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" 884 | }, 885 | "funding": [ 886 | { 887 | "url": "https://github.com/sebastianbergmann", 888 | "type": "github" 889 | } 890 | ], 891 | "time": "2020-09-28T05:30:19+00:00" 892 | }, 893 | { 894 | "name": "sebastian/comparator", 895 | "version": "4.0.8", 896 | "source": { 897 | "type": "git", 898 | "url": "https://github.com/sebastianbergmann/comparator.git", 899 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a" 900 | }, 901 | "dist": { 902 | "type": "zip", 903 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", 904 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a", 905 | "shasum": "" 906 | }, 907 | "require": { 908 | "php": ">=7.3", 909 | "sebastian/diff": "^4.0", 910 | "sebastian/exporter": "^4.0" 911 | }, 912 | "require-dev": { 913 | "phpunit/phpunit": "^9.3" 914 | }, 915 | "type": "library", 916 | "extra": { 917 | "branch-alias": { 918 | "dev-master": "4.0-dev" 919 | } 920 | }, 921 | "autoload": { 922 | "classmap": [ 923 | "src/" 924 | ] 925 | }, 926 | "notification-url": "https://packagist.org/downloads/", 927 | "license": [ 928 | "BSD-3-Clause" 929 | ], 930 | "authors": [ 931 | { 932 | "name": "Sebastian Bergmann", 933 | "email": "sebastian@phpunit.de" 934 | }, 935 | { 936 | "name": "Jeff Welch", 937 | "email": "whatthejeff@gmail.com" 938 | }, 939 | { 940 | "name": "Volker Dusch", 941 | "email": "github@wallbash.com" 942 | }, 943 | { 944 | "name": "Bernhard Schussek", 945 | "email": "bschussek@2bepublished.at" 946 | } 947 | ], 948 | "description": "Provides the functionality to compare PHP values for equality", 949 | "homepage": "https://github.com/sebastianbergmann/comparator", 950 | "keywords": [ 951 | "comparator", 952 | "compare", 953 | "equality" 954 | ], 955 | "support": { 956 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 957 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" 958 | }, 959 | "funding": [ 960 | { 961 | "url": "https://github.com/sebastianbergmann", 962 | "type": "github" 963 | } 964 | ], 965 | "time": "2022-09-14T12:41:17+00:00" 966 | }, 967 | { 968 | "name": "sebastian/complexity", 969 | "version": "2.0.2", 970 | "source": { 971 | "type": "git", 972 | "url": "https://github.com/sebastianbergmann/complexity.git", 973 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" 974 | }, 975 | "dist": { 976 | "type": "zip", 977 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", 978 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", 979 | "shasum": "" 980 | }, 981 | "require": { 982 | "nikic/php-parser": "^4.7", 983 | "php": ">=7.3" 984 | }, 985 | "require-dev": { 986 | "phpunit/phpunit": "^9.3" 987 | }, 988 | "type": "library", 989 | "extra": { 990 | "branch-alias": { 991 | "dev-master": "2.0-dev" 992 | } 993 | }, 994 | "autoload": { 995 | "classmap": [ 996 | "src/" 997 | ] 998 | }, 999 | "notification-url": "https://packagist.org/downloads/", 1000 | "license": [ 1001 | "BSD-3-Clause" 1002 | ], 1003 | "authors": [ 1004 | { 1005 | "name": "Sebastian Bergmann", 1006 | "email": "sebastian@phpunit.de", 1007 | "role": "lead" 1008 | } 1009 | ], 1010 | "description": "Library for calculating the complexity of PHP code units", 1011 | "homepage": "https://github.com/sebastianbergmann/complexity", 1012 | "support": { 1013 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 1014 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" 1015 | }, 1016 | "funding": [ 1017 | { 1018 | "url": "https://github.com/sebastianbergmann", 1019 | "type": "github" 1020 | } 1021 | ], 1022 | "time": "2020-10-26T15:52:27+00:00" 1023 | }, 1024 | { 1025 | "name": "sebastian/diff", 1026 | "version": "4.0.4", 1027 | "source": { 1028 | "type": "git", 1029 | "url": "https://github.com/sebastianbergmann/diff.git", 1030 | "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" 1031 | }, 1032 | "dist": { 1033 | "type": "zip", 1034 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", 1035 | "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", 1036 | "shasum": "" 1037 | }, 1038 | "require": { 1039 | "php": ">=7.3" 1040 | }, 1041 | "require-dev": { 1042 | "phpunit/phpunit": "^9.3", 1043 | "symfony/process": "^4.2 || ^5" 1044 | }, 1045 | "type": "library", 1046 | "extra": { 1047 | "branch-alias": { 1048 | "dev-master": "4.0-dev" 1049 | } 1050 | }, 1051 | "autoload": { 1052 | "classmap": [ 1053 | "src/" 1054 | ] 1055 | }, 1056 | "notification-url": "https://packagist.org/downloads/", 1057 | "license": [ 1058 | "BSD-3-Clause" 1059 | ], 1060 | "authors": [ 1061 | { 1062 | "name": "Sebastian Bergmann", 1063 | "email": "sebastian@phpunit.de" 1064 | }, 1065 | { 1066 | "name": "Kore Nordmann", 1067 | "email": "mail@kore-nordmann.de" 1068 | } 1069 | ], 1070 | "description": "Diff implementation", 1071 | "homepage": "https://github.com/sebastianbergmann/diff", 1072 | "keywords": [ 1073 | "diff", 1074 | "udiff", 1075 | "unidiff", 1076 | "unified diff" 1077 | ], 1078 | "support": { 1079 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1080 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" 1081 | }, 1082 | "funding": [ 1083 | { 1084 | "url": "https://github.com/sebastianbergmann", 1085 | "type": "github" 1086 | } 1087 | ], 1088 | "time": "2020-10-26T13:10:38+00:00" 1089 | }, 1090 | { 1091 | "name": "sebastian/environment", 1092 | "version": "5.1.4", 1093 | "source": { 1094 | "type": "git", 1095 | "url": "https://github.com/sebastianbergmann/environment.git", 1096 | "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" 1097 | }, 1098 | "dist": { 1099 | "type": "zip", 1100 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", 1101 | "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", 1102 | "shasum": "" 1103 | }, 1104 | "require": { 1105 | "php": ">=7.3" 1106 | }, 1107 | "require-dev": { 1108 | "phpunit/phpunit": "^9.3" 1109 | }, 1110 | "suggest": { 1111 | "ext-posix": "*" 1112 | }, 1113 | "type": "library", 1114 | "extra": { 1115 | "branch-alias": { 1116 | "dev-master": "5.1-dev" 1117 | } 1118 | }, 1119 | "autoload": { 1120 | "classmap": [ 1121 | "src/" 1122 | ] 1123 | }, 1124 | "notification-url": "https://packagist.org/downloads/", 1125 | "license": [ 1126 | "BSD-3-Clause" 1127 | ], 1128 | "authors": [ 1129 | { 1130 | "name": "Sebastian Bergmann", 1131 | "email": "sebastian@phpunit.de" 1132 | } 1133 | ], 1134 | "description": "Provides functionality to handle HHVM/PHP environments", 1135 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1136 | "keywords": [ 1137 | "Xdebug", 1138 | "environment", 1139 | "hhvm" 1140 | ], 1141 | "support": { 1142 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1143 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" 1144 | }, 1145 | "funding": [ 1146 | { 1147 | "url": "https://github.com/sebastianbergmann", 1148 | "type": "github" 1149 | } 1150 | ], 1151 | "time": "2022-04-03T09:37:03+00:00" 1152 | }, 1153 | { 1154 | "name": "sebastian/exporter", 1155 | "version": "4.0.5", 1156 | "source": { 1157 | "type": "git", 1158 | "url": "https://github.com/sebastianbergmann/exporter.git", 1159 | "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" 1160 | }, 1161 | "dist": { 1162 | "type": "zip", 1163 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", 1164 | "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", 1165 | "shasum": "" 1166 | }, 1167 | "require": { 1168 | "php": ">=7.3", 1169 | "sebastian/recursion-context": "^4.0" 1170 | }, 1171 | "require-dev": { 1172 | "ext-mbstring": "*", 1173 | "phpunit/phpunit": "^9.3" 1174 | }, 1175 | "type": "library", 1176 | "extra": { 1177 | "branch-alias": { 1178 | "dev-master": "4.0-dev" 1179 | } 1180 | }, 1181 | "autoload": { 1182 | "classmap": [ 1183 | "src/" 1184 | ] 1185 | }, 1186 | "notification-url": "https://packagist.org/downloads/", 1187 | "license": [ 1188 | "BSD-3-Clause" 1189 | ], 1190 | "authors": [ 1191 | { 1192 | "name": "Sebastian Bergmann", 1193 | "email": "sebastian@phpunit.de" 1194 | }, 1195 | { 1196 | "name": "Jeff Welch", 1197 | "email": "whatthejeff@gmail.com" 1198 | }, 1199 | { 1200 | "name": "Volker Dusch", 1201 | "email": "github@wallbash.com" 1202 | }, 1203 | { 1204 | "name": "Adam Harvey", 1205 | "email": "aharvey@php.net" 1206 | }, 1207 | { 1208 | "name": "Bernhard Schussek", 1209 | "email": "bschussek@gmail.com" 1210 | } 1211 | ], 1212 | "description": "Provides the functionality to export PHP variables for visualization", 1213 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1214 | "keywords": [ 1215 | "export", 1216 | "exporter" 1217 | ], 1218 | "support": { 1219 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1220 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" 1221 | }, 1222 | "funding": [ 1223 | { 1224 | "url": "https://github.com/sebastianbergmann", 1225 | "type": "github" 1226 | } 1227 | ], 1228 | "time": "2022-09-14T06:03:37+00:00" 1229 | }, 1230 | { 1231 | "name": "sebastian/global-state", 1232 | "version": "5.0.5", 1233 | "source": { 1234 | "type": "git", 1235 | "url": "https://github.com/sebastianbergmann/global-state.git", 1236 | "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" 1237 | }, 1238 | "dist": { 1239 | "type": "zip", 1240 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", 1241 | "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", 1242 | "shasum": "" 1243 | }, 1244 | "require": { 1245 | "php": ">=7.3", 1246 | "sebastian/object-reflector": "^2.0", 1247 | "sebastian/recursion-context": "^4.0" 1248 | }, 1249 | "require-dev": { 1250 | "ext-dom": "*", 1251 | "phpunit/phpunit": "^9.3" 1252 | }, 1253 | "suggest": { 1254 | "ext-uopz": "*" 1255 | }, 1256 | "type": "library", 1257 | "extra": { 1258 | "branch-alias": { 1259 | "dev-master": "5.0-dev" 1260 | } 1261 | }, 1262 | "autoload": { 1263 | "classmap": [ 1264 | "src/" 1265 | ] 1266 | }, 1267 | "notification-url": "https://packagist.org/downloads/", 1268 | "license": [ 1269 | "BSD-3-Clause" 1270 | ], 1271 | "authors": [ 1272 | { 1273 | "name": "Sebastian Bergmann", 1274 | "email": "sebastian@phpunit.de" 1275 | } 1276 | ], 1277 | "description": "Snapshotting of global state", 1278 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1279 | "keywords": [ 1280 | "global state" 1281 | ], 1282 | "support": { 1283 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1284 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" 1285 | }, 1286 | "funding": [ 1287 | { 1288 | "url": "https://github.com/sebastianbergmann", 1289 | "type": "github" 1290 | } 1291 | ], 1292 | "time": "2022-02-14T08:28:10+00:00" 1293 | }, 1294 | { 1295 | "name": "sebastian/lines-of-code", 1296 | "version": "1.0.3", 1297 | "source": { 1298 | "type": "git", 1299 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1300 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" 1301 | }, 1302 | "dist": { 1303 | "type": "zip", 1304 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1305 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1306 | "shasum": "" 1307 | }, 1308 | "require": { 1309 | "nikic/php-parser": "^4.6", 1310 | "php": ">=7.3" 1311 | }, 1312 | "require-dev": { 1313 | "phpunit/phpunit": "^9.3" 1314 | }, 1315 | "type": "library", 1316 | "extra": { 1317 | "branch-alias": { 1318 | "dev-master": "1.0-dev" 1319 | } 1320 | }, 1321 | "autoload": { 1322 | "classmap": [ 1323 | "src/" 1324 | ] 1325 | }, 1326 | "notification-url": "https://packagist.org/downloads/", 1327 | "license": [ 1328 | "BSD-3-Clause" 1329 | ], 1330 | "authors": [ 1331 | { 1332 | "name": "Sebastian Bergmann", 1333 | "email": "sebastian@phpunit.de", 1334 | "role": "lead" 1335 | } 1336 | ], 1337 | "description": "Library for counting the lines of code in PHP source code", 1338 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1339 | "support": { 1340 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1341 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" 1342 | }, 1343 | "funding": [ 1344 | { 1345 | "url": "https://github.com/sebastianbergmann", 1346 | "type": "github" 1347 | } 1348 | ], 1349 | "time": "2020-11-28T06:42:11+00:00" 1350 | }, 1351 | { 1352 | "name": "sebastian/object-enumerator", 1353 | "version": "4.0.4", 1354 | "source": { 1355 | "type": "git", 1356 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1357 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" 1358 | }, 1359 | "dist": { 1360 | "type": "zip", 1361 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", 1362 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", 1363 | "shasum": "" 1364 | }, 1365 | "require": { 1366 | "php": ">=7.3", 1367 | "sebastian/object-reflector": "^2.0", 1368 | "sebastian/recursion-context": "^4.0" 1369 | }, 1370 | "require-dev": { 1371 | "phpunit/phpunit": "^9.3" 1372 | }, 1373 | "type": "library", 1374 | "extra": { 1375 | "branch-alias": { 1376 | "dev-master": "4.0-dev" 1377 | } 1378 | }, 1379 | "autoload": { 1380 | "classmap": [ 1381 | "src/" 1382 | ] 1383 | }, 1384 | "notification-url": "https://packagist.org/downloads/", 1385 | "license": [ 1386 | "BSD-3-Clause" 1387 | ], 1388 | "authors": [ 1389 | { 1390 | "name": "Sebastian Bergmann", 1391 | "email": "sebastian@phpunit.de" 1392 | } 1393 | ], 1394 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1395 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1396 | "support": { 1397 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1398 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" 1399 | }, 1400 | "funding": [ 1401 | { 1402 | "url": "https://github.com/sebastianbergmann", 1403 | "type": "github" 1404 | } 1405 | ], 1406 | "time": "2020-10-26T13:12:34+00:00" 1407 | }, 1408 | { 1409 | "name": "sebastian/object-reflector", 1410 | "version": "2.0.4", 1411 | "source": { 1412 | "type": "git", 1413 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1414 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" 1415 | }, 1416 | "dist": { 1417 | "type": "zip", 1418 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1419 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1420 | "shasum": "" 1421 | }, 1422 | "require": { 1423 | "php": ">=7.3" 1424 | }, 1425 | "require-dev": { 1426 | "phpunit/phpunit": "^9.3" 1427 | }, 1428 | "type": "library", 1429 | "extra": { 1430 | "branch-alias": { 1431 | "dev-master": "2.0-dev" 1432 | } 1433 | }, 1434 | "autoload": { 1435 | "classmap": [ 1436 | "src/" 1437 | ] 1438 | }, 1439 | "notification-url": "https://packagist.org/downloads/", 1440 | "license": [ 1441 | "BSD-3-Clause" 1442 | ], 1443 | "authors": [ 1444 | { 1445 | "name": "Sebastian Bergmann", 1446 | "email": "sebastian@phpunit.de" 1447 | } 1448 | ], 1449 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1450 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1451 | "support": { 1452 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1453 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" 1454 | }, 1455 | "funding": [ 1456 | { 1457 | "url": "https://github.com/sebastianbergmann", 1458 | "type": "github" 1459 | } 1460 | ], 1461 | "time": "2020-10-26T13:14:26+00:00" 1462 | }, 1463 | { 1464 | "name": "sebastian/recursion-context", 1465 | "version": "4.0.4", 1466 | "source": { 1467 | "type": "git", 1468 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1469 | "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" 1470 | }, 1471 | "dist": { 1472 | "type": "zip", 1473 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", 1474 | "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", 1475 | "shasum": "" 1476 | }, 1477 | "require": { 1478 | "php": ">=7.3" 1479 | }, 1480 | "require-dev": { 1481 | "phpunit/phpunit": "^9.3" 1482 | }, 1483 | "type": "library", 1484 | "extra": { 1485 | "branch-alias": { 1486 | "dev-master": "4.0-dev" 1487 | } 1488 | }, 1489 | "autoload": { 1490 | "classmap": [ 1491 | "src/" 1492 | ] 1493 | }, 1494 | "notification-url": "https://packagist.org/downloads/", 1495 | "license": [ 1496 | "BSD-3-Clause" 1497 | ], 1498 | "authors": [ 1499 | { 1500 | "name": "Sebastian Bergmann", 1501 | "email": "sebastian@phpunit.de" 1502 | }, 1503 | { 1504 | "name": "Jeff Welch", 1505 | "email": "whatthejeff@gmail.com" 1506 | }, 1507 | { 1508 | "name": "Adam Harvey", 1509 | "email": "aharvey@php.net" 1510 | } 1511 | ], 1512 | "description": "Provides functionality to recursively process PHP variables", 1513 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1514 | "support": { 1515 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1516 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" 1517 | }, 1518 | "funding": [ 1519 | { 1520 | "url": "https://github.com/sebastianbergmann", 1521 | "type": "github" 1522 | } 1523 | ], 1524 | "time": "2020-10-26T13:17:30+00:00" 1525 | }, 1526 | { 1527 | "name": "sebastian/resource-operations", 1528 | "version": "3.0.3", 1529 | "source": { 1530 | "type": "git", 1531 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1532 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" 1533 | }, 1534 | "dist": { 1535 | "type": "zip", 1536 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1537 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1538 | "shasum": "" 1539 | }, 1540 | "require": { 1541 | "php": ">=7.3" 1542 | }, 1543 | "require-dev": { 1544 | "phpunit/phpunit": "^9.0" 1545 | }, 1546 | "type": "library", 1547 | "extra": { 1548 | "branch-alias": { 1549 | "dev-master": "3.0-dev" 1550 | } 1551 | }, 1552 | "autoload": { 1553 | "classmap": [ 1554 | "src/" 1555 | ] 1556 | }, 1557 | "notification-url": "https://packagist.org/downloads/", 1558 | "license": [ 1559 | "BSD-3-Clause" 1560 | ], 1561 | "authors": [ 1562 | { 1563 | "name": "Sebastian Bergmann", 1564 | "email": "sebastian@phpunit.de" 1565 | } 1566 | ], 1567 | "description": "Provides a list of PHP built-in functions that operate on resources", 1568 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1569 | "support": { 1570 | "issues": "https://github.com/sebastianbergmann/resource-operations/issues", 1571 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" 1572 | }, 1573 | "funding": [ 1574 | { 1575 | "url": "https://github.com/sebastianbergmann", 1576 | "type": "github" 1577 | } 1578 | ], 1579 | "time": "2020-09-28T06:45:17+00:00" 1580 | }, 1581 | { 1582 | "name": "sebastian/type", 1583 | "version": "3.2.0", 1584 | "source": { 1585 | "type": "git", 1586 | "url": "https://github.com/sebastianbergmann/type.git", 1587 | "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" 1588 | }, 1589 | "dist": { 1590 | "type": "zip", 1591 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", 1592 | "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", 1593 | "shasum": "" 1594 | }, 1595 | "require": { 1596 | "php": ">=7.3" 1597 | }, 1598 | "require-dev": { 1599 | "phpunit/phpunit": "^9.5" 1600 | }, 1601 | "type": "library", 1602 | "extra": { 1603 | "branch-alias": { 1604 | "dev-master": "3.2-dev" 1605 | } 1606 | }, 1607 | "autoload": { 1608 | "classmap": [ 1609 | "src/" 1610 | ] 1611 | }, 1612 | "notification-url": "https://packagist.org/downloads/", 1613 | "license": [ 1614 | "BSD-3-Clause" 1615 | ], 1616 | "authors": [ 1617 | { 1618 | "name": "Sebastian Bergmann", 1619 | "email": "sebastian@phpunit.de", 1620 | "role": "lead" 1621 | } 1622 | ], 1623 | "description": "Collection of value objects that represent the types of the PHP type system", 1624 | "homepage": "https://github.com/sebastianbergmann/type", 1625 | "support": { 1626 | "issues": "https://github.com/sebastianbergmann/type/issues", 1627 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" 1628 | }, 1629 | "funding": [ 1630 | { 1631 | "url": "https://github.com/sebastianbergmann", 1632 | "type": "github" 1633 | } 1634 | ], 1635 | "time": "2022-09-12T14:47:03+00:00" 1636 | }, 1637 | { 1638 | "name": "sebastian/version", 1639 | "version": "3.0.2", 1640 | "source": { 1641 | "type": "git", 1642 | "url": "https://github.com/sebastianbergmann/version.git", 1643 | "reference": "c6c1022351a901512170118436c764e473f6de8c" 1644 | }, 1645 | "dist": { 1646 | "type": "zip", 1647 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", 1648 | "reference": "c6c1022351a901512170118436c764e473f6de8c", 1649 | "shasum": "" 1650 | }, 1651 | "require": { 1652 | "php": ">=7.3" 1653 | }, 1654 | "type": "library", 1655 | "extra": { 1656 | "branch-alias": { 1657 | "dev-master": "3.0-dev" 1658 | } 1659 | }, 1660 | "autoload": { 1661 | "classmap": [ 1662 | "src/" 1663 | ] 1664 | }, 1665 | "notification-url": "https://packagist.org/downloads/", 1666 | "license": [ 1667 | "BSD-3-Clause" 1668 | ], 1669 | "authors": [ 1670 | { 1671 | "name": "Sebastian Bergmann", 1672 | "email": "sebastian@phpunit.de", 1673 | "role": "lead" 1674 | } 1675 | ], 1676 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1677 | "homepage": "https://github.com/sebastianbergmann/version", 1678 | "support": { 1679 | "issues": "https://github.com/sebastianbergmann/version/issues", 1680 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" 1681 | }, 1682 | "funding": [ 1683 | { 1684 | "url": "https://github.com/sebastianbergmann", 1685 | "type": "github" 1686 | } 1687 | ], 1688 | "time": "2020-09-28T06:39:44+00:00" 1689 | }, 1690 | { 1691 | "name": "theseer/tokenizer", 1692 | "version": "1.2.1", 1693 | "source": { 1694 | "type": "git", 1695 | "url": "https://github.com/theseer/tokenizer.git", 1696 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" 1697 | }, 1698 | "dist": { 1699 | "type": "zip", 1700 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", 1701 | "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", 1702 | "shasum": "" 1703 | }, 1704 | "require": { 1705 | "ext-dom": "*", 1706 | "ext-tokenizer": "*", 1707 | "ext-xmlwriter": "*", 1708 | "php": "^7.2 || ^8.0" 1709 | }, 1710 | "type": "library", 1711 | "autoload": { 1712 | "classmap": [ 1713 | "src/" 1714 | ] 1715 | }, 1716 | "notification-url": "https://packagist.org/downloads/", 1717 | "license": [ 1718 | "BSD-3-Clause" 1719 | ], 1720 | "authors": [ 1721 | { 1722 | "name": "Arne Blankerts", 1723 | "email": "arne@blankerts.de", 1724 | "role": "Developer" 1725 | } 1726 | ], 1727 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1728 | "support": { 1729 | "issues": "https://github.com/theseer/tokenizer/issues", 1730 | "source": "https://github.com/theseer/tokenizer/tree/1.2.1" 1731 | }, 1732 | "funding": [ 1733 | { 1734 | "url": "https://github.com/theseer", 1735 | "type": "github" 1736 | } 1737 | ], 1738 | "time": "2021-07-28T10:34:58+00:00" 1739 | } 1740 | ], 1741 | "aliases": [], 1742 | "minimum-stability": "stable", 1743 | "stability-flags": [], 1744 | "prefer-stable": false, 1745 | "prefer-lowest": false, 1746 | "platform": [], 1747 | "platform-dev": [], 1748 | "plugin-api-version": "2.3.0" 1749 | } 1750 | -------------------------------------------------------------------------------- /conf/apache-extra.conf: -------------------------------------------------------------------------------- 1 | 2 | Options -Indexes 3 | AllowOverride All 4 | 5 | AuthType Basic 6 | AuthName b 7 | AuthUserFile /app/db/htusers 8 | require valid-user 9 | 10 | 11 | 12 | DocumentRoot /app/htdocs 13 | ErrorLog /dev/stderr 14 | CustomLog /dev/stdout combined 15 | 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | app: 5 | build: . 6 | volumes: 7 | - "./:/app:delegated" 8 | - "./conf/apache-extra.conf:/etc/apache2/conf-enabled/extra.conf" 9 | ports: 10 | - "127.0.0.1:9090:80" 11 | user: "${USERID}:${GID}" 12 | environment: 13 | - APACHE_RUN_USER=#${USERID} 14 | - APACHE_RUN_GROUP=#${GID} 15 | - INFINITE_SCROLLING 16 | working_dir: /app 17 | -------------------------------------------------------------------------------- /htdocs/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteBase / 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule . index.php [L] 6 | -------------------------------------------------------------------------------- /htdocs/bookmarkManager.js: -------------------------------------------------------------------------------- 1 | ;(function() { 2 | const contentEl = document.getElementById('content') 3 | const formEl = document.getElementById('filterform') 4 | const queryEl = document.getElementById('query') 5 | 6 | async function request(data) { 7 | const res = await fetch('', { 8 | method: 'POST', 9 | contentType: 'application/json; charset=utf-8', 10 | body: JSON.stringify(data), 11 | credentials: 'include' 12 | }) 13 | 14 | const text = await res.text() 15 | return JSON.parse(text) 16 | } 17 | 18 | async function deleteEntry(id) { 19 | const res = await request({ action: 'delete', id }) 20 | 21 | if (res.message) { 22 | alert(res.message) 23 | } 24 | 25 | if (res.result === true) { 26 | document.getElementById(`entry_${res.id}`).remove() 27 | } 28 | } 29 | 30 | async function setTitle(id, title) { 31 | const res = await request({ action: 'settitle', id, title }) 32 | 33 | if (res.message) { 34 | alert(res.message) 35 | } 36 | 37 | if (res.result === true) { 38 | const el = document.getElementById(`entry_${res.id}`) 39 | el.querySelector('.title').innerHTML = res.title 40 | el.setAttribute('data-title', res.rawTitle) 41 | el.querySelector('.tags').innerHTML = res.tags 42 | } else { 43 | alert('err') 44 | } 45 | } 46 | 47 | async function setLink(id, link) { 48 | const res = await request({ action: 'setlink', id, link }) 49 | 50 | if (res.message) { 51 | alert(res.message) 52 | } 53 | 54 | if (res.result === true) { 55 | const el = document.getElementById(`entry_${res.id}`) 56 | const linkEl = el.querySelector('.link') 57 | linkEl.innerHTML = res.link 58 | linkEl.setAttribute('href', res.link) 59 | } else { 60 | alert('err') 61 | } 62 | } 63 | 64 | async function addUrl(url, force) { 65 | queryEl.setAttribute('disabled', 'disabled') 66 | 67 | const res = await request({ 68 | action: 'add', 69 | url, 70 | force: force ? '1' : '0', 71 | }) 72 | 73 | if (!res.force && res.message === 'could not fetch') { 74 | if (confirm('could not fetch, add anyway?')) { 75 | addUrl(res.url, true) 76 | return 77 | } 78 | 79 | queryEl.removeAttribute('disabled') 80 | queryEl.focus() 81 | return 82 | } 83 | 84 | if (res.message) { 85 | alert(res.message) 86 | } 87 | 88 | if (res.result === true) { 89 | document.location.reload() 90 | } 91 | 92 | queryEl.removeAttribute('disabled') 93 | queryEl.focus() 94 | } 95 | 96 | contentEl.addEventListener('click', e => { 97 | const target = e.target 98 | 99 | if (target && target.className === 'title') { 100 | const id = target.parentNode.getAttribute('data-id'), 101 | rawTitle = target.parentNode.getAttribute('data-title'), 102 | ret = prompt('Rename or enter "-" to delete', rawTitle) 103 | 104 | if (!ret) { 105 | return 106 | } 107 | 108 | if (ret === '-') { 109 | if (confirm('Really delete?')) { 110 | deleteEntry(id) 111 | return 112 | } else { 113 | return 114 | } 115 | } 116 | 117 | setTitle(id, ret) 118 | } 119 | }) 120 | 121 | contentEl.addEventListener('dblclick', e => { 122 | const target = e.target 123 | 124 | if (target && target.className === 'entry') { 125 | const id = target.getAttribute('data-id') 126 | const href = target.querySelector('.link').getAttribute('href') 127 | const ret = prompt('Edit bookmark', href) 128 | 129 | if (ret === null) { 130 | return 131 | } 132 | 133 | setLink(id, ret) 134 | } 135 | }) 136 | 137 | formEl.addEventListener('submit', e => { 138 | const query = queryEl.value 139 | 140 | e.preventDefault() 141 | 142 | if (query.indexOf('http:') === 0 || query.indexOf('https:') === 0) { 143 | addUrl(query) 144 | return false 145 | } 146 | 147 | document.location.href = '?filter=' + encodeURIComponent(query) 148 | 149 | return false 150 | }) 151 | 152 | let loadingMore = false 153 | let ifStep = window.infiniteScrolling 154 | let ifSkip = ifStep 155 | 156 | async function loadMore() { 157 | if (loadingMore) { 158 | return 159 | } 160 | 161 | loadingMore = true 162 | 163 | const url = 164 | `?filter=${encodeURIComponent(window.filter)}` + 165 | `&format=html&count=${ifStep}&skip=${ifSkip}` 166 | const res = await fetch(url, { credentials: 'include' }) 167 | const text = await res.text() 168 | 169 | if (!text) { 170 | return 171 | } 172 | 173 | contentEl.insertAdjacentHTML('beforeend', text) 174 | ifSkip += ifStep 175 | loadingMore = false 176 | } 177 | 178 | if (window.infiniteScrolling) { 179 | window.addEventListener('scroll', e => { 180 | const offset = 181 | document.body.offsetHeight - (window.pageYOffset + window.innerHeight) 182 | 183 | if (offset < 500) { 184 | loadMore() 185 | } 186 | }) 187 | } 188 | })() 189 | 190 | -------------------------------------------------------------------------------- /htdocs/bookmarklet.php: -------------------------------------------------------------------------------- 1 | 24 | 25 | Drag this to your bookmarks bar: 26 | 27 | Bookmark 28 | -------------------------------------------------------------------------------- /htdocs/index.php: -------------------------------------------------------------------------------- 1 | subPage === 'bookmarklet') { 11 | include __DIR__.'/bookmarklet.php'; 12 | exit(); 13 | } elseif ($b->subPage) { 14 | throw new \Exception('Page not found', 404); 15 | } 16 | 17 | if ($_SERVER['REQUEST_METHOD'] === 'POST') { 18 | $postData = json_decode(file_get_contents("php://input"), true); 19 | $b->handleAjaxRequest($postData); 20 | } 21 | 22 | if (!empty($_GET['filter'])) { 23 | $filter = $_GET['filter']; 24 | } else { 25 | $filter = false; 26 | } 27 | 28 | $skip = false; 29 | $count = $b->getConfig('infiniteScrolling'); 30 | if ($count !== null) { 31 | $skip = 0; 32 | } 33 | 34 | if (isset($_GET['skip'])) { 35 | $skip = $_GET['skip']; 36 | } 37 | 38 | if (isset($_GET['count'])) { 39 | $count = $_GET['count']; 40 | } 41 | 42 | if (!empty($_GET['format'])) { 43 | $format = $_GET['format']; 44 | } else { 45 | $format = false; 46 | } 47 | 48 | $entries = $b->getDB()->getEntries($filter, $skip, $count); 49 | 50 | if ($format === 'json') { 51 | header('Content-Type: application/json; charset=utf-8'); 52 | echo json_encode($entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 53 | exit(); 54 | } 55 | 56 | if ($format === 'html') { 57 | dumpEntries($entries); 58 | exit(); 59 | } 60 | } catch (\Exception $e) { 61 | if (($code = $e->getCode()) && is_numeric($code)) { 62 | http_response_code($code); 63 | } else { 64 | http_response_code(500); 65 | } 66 | 67 | echo $e->getMessage(); 68 | exit(); 69 | } 70 | 71 | function dumpEntries($entries) 72 | { 73 | foreach ($entries as $entry) { 74 | ?> 75 |
76 |
77 | 78 |
79 |
80 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | b 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | 103 |
104 |
105 | 112 |
113 |
114 | 115 | 116 | 117 |
118 | 119 | 126 | 127 | 128 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /htdocs/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } 376 | -------------------------------------------------------------------------------- /htdocs/style.css: -------------------------------------------------------------------------------- 1 | 2 | /*** common ***/ 3 | 4 | html { 5 | font-family: Arial, sans-serif; 6 | font-size: 16px; 7 | } 8 | 9 | body { 10 | background-color: #fff; 11 | margin: 10px; 12 | } 13 | 14 | a { 15 | color: #2F2318; 16 | } 17 | 18 | a.hash { 19 | color: #6979A8; 20 | } 21 | 22 | a.link { 23 | color: #869DD4; 24 | } 25 | 26 | #content { 27 | margin-left: auto; 28 | margin-right: auto; 29 | min-width: 800px; 30 | max-width: 1000px; 31 | } 32 | 33 | /*** header ***/ 34 | 35 | .header { 36 | padding: 0 0 10px 0; 37 | } 38 | 39 | input { 40 | padding: 2px; 41 | margin: 0; 42 | width: 98%; 43 | } 44 | 45 | /*** entry ***/ 46 | 47 | .entry { 48 | display: block; 49 | padding-bottom: 10px; 50 | padding-top: 10px; 51 | } 52 | 53 | .tags { 54 | font-size: smaller; 55 | } 56 | 57 | .entry .title { 58 | width: 100%; 59 | cursor: pointer; 60 | } 61 | 62 | .entry .title:hover { 63 | background-color: #FFFAD8; 64 | } 65 | 66 | .entry .link { 67 | color: #ccc; 68 | word-wrap: break-word; 69 | font-size: smaller; 70 | } 71 | 72 | /*** responsive ***/ 73 | 74 | @media (max-width: 800px) { 75 | 76 | #content { 77 | min-width: 100%; 78 | max-width: 100%; 79 | } 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /htpasswd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USERID=$(id -u) GID=$(id -g) docker compose run --rm --user "$(id -u):$(id -g)" app htpasswd "$@" 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | tests 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/BookmarkManager.php: -------------------------------------------------------------------------------- 1 | */ 8 | protected array $config; 9 | protected DB $db; 10 | protected string $user; 11 | protected string $baseDir; 12 | public string $requestUri; 13 | public string $subPage = ''; 14 | 15 | public function __construct(string $requestUri = '/') 16 | { 17 | $getEnv = function (string $key): ?string { 18 | $value = getenv($key); 19 | return $value !== false ? $value : null; 20 | }; 21 | 22 | $this->config = [ 23 | 'baseDir' => $getEnv('BASE_DIR') ?? '/app/db/', 24 | 'baseUri' => $getEnv('BASE_URI') ?? '/', 25 | 'infiniteScrolling' => $getEnv('INFINITE_SCROLLING') ?? null, 26 | ]; 27 | 28 | $this->baseDir = $this->config['baseDir']; 29 | 30 | if (!is_dir($this->baseDir)) { 31 | throw new \Exception('invalid baseDir: ' . $this->baseDir); 32 | } 33 | 34 | if (!is_writeable($this->baseDir)) { 35 | throw new \Exception('baseDir not writeable: ' . $this->baseDir); 36 | } 37 | 38 | $baseUri = $this->config['baseUri']; 39 | 40 | if (strpos($requestUri, $baseUri) !== 0) { 41 | throw new \Exception('invalid baseuri'); 42 | } 43 | 44 | /* cut baseUri from request uri */ 45 | $this->requestUri = substr($requestUri, strlen($baseUri)); 46 | 47 | /* cut query string from request uri */ 48 | if (($p = strpos($this->requestUri, '?')) !== false) { 49 | $this->requestUri = substr($this->requestUri, 0, $p); 50 | } 51 | 52 | $uriParts = explode('/', trim($this->requestUri, '/')); 53 | 54 | if (count($uriParts) === 2) { 55 | $this->subPage = $uriParts[1]; 56 | } 57 | 58 | $user = $uriParts[0]; 59 | 60 | if (!preg_match('@^[a-z0-9]+$@i', $user)) { 61 | throw new \Exception('Page not found', 404); 62 | } 63 | 64 | if (!is_dir($this->baseDir.$user)) { 65 | throw new \Exception('No such user: '.$user, 404); 66 | } 67 | 68 | if (!is_writeable($this->baseDir.$user)) { 69 | throw new \Exception('User dir not writeable'); 70 | } 71 | 72 | $this->user = $user; 73 | 74 | $this->db = new DB($this->baseDir.$user.'/b.db'); 75 | } 76 | 77 | public function getConfig(string $key): ?string 78 | { 79 | if (isset($this->config[$key])) { 80 | return $this->config[$key]; 81 | } 82 | 83 | return null; 84 | } 85 | 86 | public function getDB(): DB 87 | { 88 | return $this->db; 89 | } 90 | 91 | public function handleAjaxRequest(array $postData): void 92 | { 93 | if (empty($postData['action'])) { 94 | return; 95 | } 96 | 97 | $action = (string)$postData['action']; 98 | $error = true; 99 | $result = []; 100 | 101 | if (!empty($postData['id'])) { 102 | $id = (int)$postData['id']; 103 | $result['id'] = $id; 104 | } else { 105 | $id = 0; 106 | } 107 | 108 | try { 109 | switch ($action) { 110 | case 'add': 111 | $result['url'] = (string)$postData['url']; 112 | $result['force'] = $postData['force'] ? true : false; 113 | list($url, $desc) = array_pad(explode(' ', (string)$postData['url'], 2), 2, ''); 114 | $this->addBookmark($url, $desc, !empty($postData['force'])); 115 | $error = false; 116 | break; 117 | 118 | case 'delete': 119 | if ($id) { 120 | $this->db->deleteEntry($id); 121 | $error = false; 122 | } 123 | break; 124 | 125 | case 'settitle': 126 | if ($id && !empty($postData['title'])) { 127 | $this->db->setTitle($id, (string)$postData['title']); 128 | $error = false; 129 | $result['title'] = self::formatDesc((string)$postData['title'], false); 130 | $result['rawTitle'] = (string)$postData['title']; 131 | $result['tags'] = self::formatTags((string)$postData['title']); 132 | } 133 | break; 134 | 135 | case 'setlink': 136 | if ($id && !empty($postData['link'])) { 137 | $this->db->setLink($id, (string)$postData['link']); 138 | $error = false; 139 | $result['link'] = (string)$postData['link']; 140 | } 141 | break; 142 | 143 | default: 144 | return; 145 | } 146 | } catch (\Exception $e) { 147 | $result['message'] = $e->getMessage(); 148 | } 149 | 150 | if ($error) { 151 | $result['result'] = false; 152 | } else { 153 | $result['result'] = true; 154 | } 155 | 156 | header('Content-Type: application/json; charset=utf-8'); 157 | echo json_encode($result, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE); 158 | 159 | exit(); 160 | } 161 | 162 | public function addBookmark(string $url, string $appendDesc = '', bool $force = false): bool 163 | { 164 | if ($this->db->exists($url)) { 165 | throw new \Exception('Bookmark already exists.'); 166 | } 167 | 168 | try { 169 | $body = $this->fetch($url); 170 | } catch (\Exception $e) { 171 | if (!$force) { 172 | throw $e; 173 | } 174 | } 175 | 176 | if (!empty($body)) { 177 | $desc = $this->extractTitle($body).' '.$appendDesc; 178 | } else { 179 | $desc = $url; 180 | } 181 | 182 | return $this->db->add($desc, $url); 183 | } 184 | 185 | private function fetch(string $url): string 186 | { 187 | if (($h = curl_init($url)) === false) { 188 | throw new \Exception('could not init curl'); 189 | } 190 | 191 | curl_setopt($h, CURLOPT_RETURNTRANSFER, true); 192 | curl_setopt($h, CURLOPT_FOLLOWLOCATION, true); 193 | 194 | if (($ret = curl_exec($h)) === false) { 195 | throw new \Exception('could not fetch'); 196 | } 197 | 198 | return (string)$ret; 199 | } 200 | 201 | public static function extractTitle(string $body): string 202 | { 203 | if (!preg_match('@([^<]+)@', $body, $m)) { 204 | return '(unknown title)'; 205 | } 206 | 207 | $ret = $m[1]; 208 | 209 | $enc = mb_detect_encoding($ret, 'UTF-8,ISO-8859-1', true); 210 | 211 | if ($enc !== 'UTF-8') { 212 | $ret = mb_convert_encoding($ret, 'UTF-8', $enc); 213 | } 214 | 215 | $ret = trim(html_entity_decode($ret, ENT_QUOTES, 'UTF-8')); 216 | 217 | return $ret; 218 | } 219 | 220 | public static function formatDesc(string $desc, bool $withTags = true): string 221 | { 222 | $desc = htmlspecialchars($desc); 223 | 224 | if (preg_match_all('@#[a-z0-9-_]+@i', $desc, $m)) { 225 | $matches = $m[0]; 226 | 227 | foreach ($matches as $tag) { 228 | if ($withTags) { 229 | $replaceWith = self::formatTagLink($tag); 230 | } else { 231 | $replaceWith = ''; 232 | } 233 | $desc = str_replace($tag, $replaceWith, $desc); 234 | } 235 | } 236 | 237 | return trim($desc); 238 | } 239 | 240 | public static function formatTags(string $desc): string 241 | { 242 | $tags = ''; 243 | $desc = htmlspecialchars($desc); 244 | 245 | if (preg_match_all('@#[a-z0-9-_]+@i', $desc, $m)) { 246 | $matches = $m[0]; 247 | 248 | foreach ($matches as $tag) { 249 | $tags .= self::formatTagLink($tag).' '; 250 | } 251 | } 252 | 253 | return $tags; 254 | } 255 | 256 | public static function formatTagLink(string $tag): string 257 | { 258 | return '<a class="hash" href="?filter='.rawurlencode($tag).'">'.$tag.'</a>'; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/DB.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace B; 4 | 5 | class DB 6 | { 7 | protected \PDO $pdo; 8 | 9 | public function __construct(string $file = '') 10 | { 11 | $new = !file_exists($file); 12 | 13 | $this->pdo = new \PDO('sqlite:'.$file); 14 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 15 | $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); 16 | 17 | if ($new) { 18 | $this->createTables(); 19 | } 20 | } 21 | 22 | private function createTables(): void 23 | { 24 | $this->pdo->prepare(" 25 | CREATE TABLE b ( 26 | id INTEGER PRIMARY KEY, 27 | date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 28 | desc TEXT NOT NULL DEFAULT '', 29 | link TEXT NOT NULL DEFAULT '' UNIQUE 30 | ); 31 | ")->execute(); 32 | } 33 | 34 | public function add(string $desc, string $link): bool 35 | { 36 | $this->pdo->prepare(' 37 | INSERT INTO b (desc, link) VALUES (:desc, :link) 38 | ')->execute([ 39 | ':desc' => $desc, 40 | ':link' => $link, 41 | ]); 42 | 43 | return true; 44 | } 45 | 46 | public function exists(string $link): bool 47 | { 48 | $st = $this->pdo->prepare(' 49 | SELECT id FROM b 50 | WHERE link = :link 51 | '); 52 | 53 | $st->execute([ ':link' => $link ]); 54 | 55 | return (bool) $st->fetch(); 56 | } 57 | 58 | public function getEntries(?string $filter = null, ?int $skip = null, ?int $count = null): array 59 | { 60 | if ($skip !== null && $count !== null) { 61 | $limit = 'LIMIT :skip, :count'; 62 | } else { 63 | $limit = ''; 64 | } 65 | 66 | $where = []; 67 | $args = []; 68 | 69 | if ($filter !== null) { 70 | $queryParts = explode(' ', $filter); 71 | foreach ($queryParts as $i => $part) { 72 | $where[] = "desc LIKE :filter$i"; 73 | $args[":filter$i"] = '%'.$part.'%'; 74 | } 75 | } else { 76 | $where[] = "desc LIKE :filter"; 77 | $args[":filter"] = '%'; 78 | } 79 | 80 | $st = $this->pdo->prepare(" 81 | SELECT id, desc, link FROM b 82 | WHERE ". join(' AND ', $where) ." 83 | ORDER BY date DESC 84 | $limit 85 | "); 86 | 87 | if ($skip !== null && $count !== null) { 88 | $args['skip'] = $skip; 89 | $args['count'] = $count; 90 | } 91 | 92 | $st->execute($args); 93 | 94 | $ret = $st->fetchAll(); 95 | 96 | return $ret; 97 | } 98 | 99 | public function deleteEntry(int $id): bool 100 | { 101 | $this->pdo->prepare(' 102 | DELETE FROM b WHERE id = :id 103 | ')->execute([ 104 | ':id' => $id, 105 | ]); 106 | 107 | return true; 108 | } 109 | 110 | public function setTitle(int $id, string $title): bool 111 | { 112 | $this->pdo->prepare(' 113 | UPDATE b SET desc = :desc WHERE id = :id 114 | ')->execute([ 115 | ':id' => $id, 116 | ':desc' => $title, 117 | ]); 118 | 119 | return true; 120 | } 121 | 122 | public function setLink(int $id, string $link): bool 123 | { 124 | $this->pdo->prepare(' 125 | UPDATE b SET link = :link WHERE id = :id 126 | ')->execute([ 127 | ':id' => $id, 128 | ':link' => $link, 129 | ]); 130 | 131 | return true; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/BookmarkManagerTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use PHPUnit\Framework\TestCase; 4 | 5 | class BookmarkManagerTest extends TestCase 6 | { 7 | public function test_extractTitle() 8 | { 9 | $nbsp = html_entity_decode(' ', 0, 'UTF-8'); 10 | 11 | $tests = [ 12 | 'extractTitle.html' => 'bla\'bla möö'.$nbsp.'p&hop', 13 | 'extractTitle.latin1.html' => 'mööp', 14 | ]; 15 | 16 | foreach ($tests as $file => $exp) { 17 | $title = file_get_contents(dirname(__FILE__).'/'.$file); 18 | $title = B\BookmarkManager::extractTitle($title); 19 | $this->assertEquals($exp, $title); 20 | $this->assertEquals('UTF-8', mb_detect_encoding($title, 'UTF-8', true)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | require_once __DIR__.'/../vendor/autoload.php'; 4 | -------------------------------------------------------------------------------- /tests/extractTitle.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | 4 | <title> 5 | 6 | 7 | bla'bla möö p&hop 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/extractTitle.latin1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebcode/b/d0d240bd99d12a43eb9740033854baeea09bf797/tests/extractTitle.latin1.html --------------------------------------------------------------------------------