├── .editorconfig ├── .gitignore ├── .htaccess ├── API.md ├── AUTHORS.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── api ├── .htaccess └── index.php ├── assets ├── css │ ├── base.css │ └── bootstrap-3.4.1.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── js │ ├── bootstrap-3.4.1.min.js │ └── jquery-3.7.0.slim.min.js ├── logo.svg └── logo_dark.svg ├── composer.json ├── composer.lock ├── data ├── .htaccess ├── data.sql ├── migration-1.sql ├── migration-2.sql ├── migration-3.sql ├── migration-4.sql ├── migration-5.sql ├── migration-6.sql └── migration-7.sql ├── index.php ├── logs └── .htaccess ├── src ├── .htaccess ├── Helpers │ ├── Tokens.php │ └── Utils.php ├── constants.php ├── dependencies.php ├── middleware.php ├── queries.php ├── routes │ ├── asset.php │ ├── asset_edit.php │ ├── auth.php │ └── user.php ├── settings-local-example.php └── settings.php └── templates ├── .htaccess ├── _asset_fields.phtml ├── _csrf.phtml ├── _footer.phtml ├── _header.phtml ├── _pagination.phtml ├── asset.phtml ├── asset_edit.phtml ├── asset_edits.phtml ├── assets.phtml ├── change_password.phtml ├── edit_asset.phtml ├── edit_asset_edit.phtml ├── error.phtml ├── feed.phtml ├── forgot_password.phtml ├── forgot_password_result.phtml ├── index.phtml ├── login.phtml ├── register.phtml ├── reset_password.phtml ├── reset_password_email.phtml └── submit_asset.phtml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | # CSS 12 | [*.css] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # PHP 17 | [*.{php,phtml}] 18 | indent_style = space 19 | indent_size = 4 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | logs/**/*.log 3 | src/settings-local.php -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | 8 | RewriteBase /asset-library 9 | 10 | RewriteCond %{REQUEST_FILENAME} !-f 11 | RewriteRule ^ index.php [QSA,L] 12 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Asset library API 2 | 3 | All POST APIs accept JSON-encoded or formdata-encoded bodies. 4 | GET APIs accept standard query strings. 5 | 6 | ## Core 7 | 8 | The core part of the API is understood and used by the C++ frontend embedded in the Godot editor. It has to stay compatible with all versions of Godot. 9 | 10 | * [`GET /configure`](#api-get-configure) - get details such as category and login URL 11 | * [`GET /asset?…`](#api-get-asset) - list assets by filter 12 | * [`GET /asset/{id}`](#api-get-asset-id) - get asset by id 13 | 14 | ## Auth API 15 | 16 |
17 | 18 | ### `POST /register` 19 | ```json 20 | { 21 | "username": "(username)", 22 | "password": "(password)", 23 | "email": "(email)" 24 | } 25 | ``` 26 | Successful result: 27 | ```json 28 | { 29 | "username": "(username)", 30 | "registered": true 31 | } 32 | ``` 33 | 34 | 35 | Register a user, given a username, password, and email. 36 | 37 |
38 | 39 | ### `POST /login` 40 | ```json 41 | { 42 | "username": "(username)", 43 | "password": "(password)" 44 | } 45 | ``` 46 | Successful result: 47 | ```json 48 | { 49 | "authenticated": true, 50 | "username": "(username)", 51 | "token": "(token)" 52 | } 53 | ``` 54 | 55 | Login as a given user. Results in a token which can be used for authenticated requests. 56 | 57 |
58 | 59 | ### `POST /logout` 60 | ```json 61 | { 62 | "token": "(token)" 63 | } 64 | ``` 65 | Successful result: 66 | ```json 67 | { 68 | "authenticated": false, 69 | "token": "" 70 | } 71 | ``` 72 | 73 | Logout a user, given a token. The token is invalidated in the process. 74 | 75 | 76 |
77 | 78 | ### `POST /change_password` 79 | ```json 80 | { 81 | "token": "(token)", 82 | "old_password": "(password)", 83 | "new_password": "(new password)" 84 | } 85 | ``` 86 | Successful result: 87 | ```json 88 | { 89 | "token": "" 90 | } 91 | ``` 92 | 93 | Change a user's password. The token is invalidated in the process. 94 | 95 | 96 |
97 | 98 | ### `GET /configure` 99 | ```http 100 | ?type=(any|addon|project) 101 | &session 102 | ``` 103 | Example result: 104 | ```json 105 | { 106 | "categories": [ 107 | { 108 | "id": "1", 109 | "name": "2D Tools", 110 | "type": "0" 111 | }, 112 | { 113 | "id": "2", 114 | "name": "Templates", 115 | "type": "1" 116 | }, 117 | ], 118 | "token": "…", 119 | "login_url": "https://…" 120 | } 121 | ``` 122 | 123 | Get a list of categories (needed for filtering assets) and potentially a login URL which can be given to the user in order to authenticate him in the engine (currently unused and not working). 124 | 125 | ## Assets API 126 | 127 |
128 | 129 | ### `GET /asset?…` 130 | ```http 131 | ?type=(any|addon|project) 132 | &category=(category id) 133 | &support=(official|featured|community|testing) 134 | &filter=(search text) 135 | &user=(submitter username) 136 | &cost=(license) 137 | &godot_version=(major).(minor).(patch) 138 | &max_results=(number 1…500) 139 | &page=(number, pages to skip) OR &offset=(number, rows to skip) 140 | &sort=(rating|cost|name|updated) 141 | &reverse 142 | ``` 143 | Example response: 144 | ```json 145 | { 146 | "result": [ 147 | { 148 | "asset_id": "1", 149 | "title": "Snake", 150 | "author": "test", 151 | "author_id": "1", 152 | "category": "2D Tools", 153 | "category_id": "1", 154 | "godot_version": "2.1", 155 | "rating": "0", 156 | "cost": "GPLv3", 157 | "support_level": "testing", 158 | "icon_url": "https://….png", 159 | "version": "1", 160 | "version_string": "alpha", 161 | "modify_date": "2018-08-21 15:49:00" 162 | } 163 | ], 164 | "page": 0, 165 | "pages": 0, 166 | "page_length": 10, 167 | "total_items": 1 168 | } 169 | ``` 170 | 171 | Get a list of assets. 172 | 173 | Some notes: 174 | * Leading and trailing whitespace in `filter` is trimmed on the server side. 175 | * For legacy purposes, not supplying godot version would list only 2.1 assets, while not supplying type would list only addons. 176 | * To specify multiple support levels, join them with `+`, e.g. `support=featured+community`. 177 | * Godot version can be specified as you see fit, for example, `godot_version=3.1` or `godot_version=3.1.5`. Currently, the patch version is disregarded, but may be honored in the future. 178 | 179 |
180 | 181 | ### `GET /asset/{id}` 182 | No query params. 183 | Example result: 184 | ```json 185 | { 186 | "asset_id": "1", 187 | "type": "addon", 188 | "title": "Snake", 189 | "author": "test", 190 | "author_id": "1", 191 | "version": "1", 192 | "version_string": "alpha", 193 | "category": "2D Tools", 194 | "category_id": "1", 195 | "godot_version": "2.1", 196 | "rating": "0", 197 | "cost": "GPLv3", 198 | "description": "Lorem ipsum…", 199 | "support_level": "testing", 200 | "download_provider": "GitHub", 201 | "download_commit": "master", 202 | "download_hash": "(sha256 hash of the downloaded zip)", 203 | "browse_url": "https://github.com/…", 204 | "issues_url": "https://github.com/…/issues", 205 | "icon_url": "https://….png", 206 | "searchable": "1", 207 | "modify_date": "2018-08-21 15:49:00", 208 | "download_url": "https://github.com/…/archive/master.zip", 209 | "previews": [ 210 | { 211 | "preview_id": "1", 212 | "type": "video", 213 | "link": "https://www.youtube.com/watch?v=…", 214 | "thumbnail": "https://img.youtube.com/vi/…/default.jpg" 215 | }, 216 | { 217 | "preview_id": "2", 218 | "type": "image", 219 | "link": "https://….png", 220 | "thumbnail": "https://….png" 221 | } 222 | ] 223 | } 224 | ``` 225 | 226 | Notes: 227 | * The `cost` field is the license. Other asset libraries may put the price there and supply a download URL which requires authentication. 228 | * In the official asset library, the `download_hash` field is always empty and is kept for compatibility only. The editor will skip hash checks if `download_hash` is an empty string. Third-party asset libraries may specify a SHA-256 hash to be used by the editor to verify the download integrity. 229 | * The download URL is generated based on the download commit and the browse URL. 230 | 231 |
232 | 233 | ### `POST /asset/{id}/delete` 234 | ```json 235 | { 236 | "token": "…" 237 | } 238 | ``` 239 | Successful response: 240 | ```json 241 | { 242 | "changed": true 243 | } 244 | ``` 245 | 246 | Soft-delete an asset. Useable by moderators and the owner of the asset. 247 | 248 |
249 | 250 | ### `POST /asset/{id}/undelete` 251 | ```json 252 | { 253 | "token": "…" 254 | } 255 | ``` 256 | Successful response: 257 | ```json 258 | { 259 | "changed": true 260 | } 261 | ``` 262 | 263 | Revert a deletion of an asset. Useable by moderators and the owner of the asset. 264 | 265 |
266 | 267 | ### `POST /asset/{id}/support_level` 268 | ```json 269 | { 270 | "support_level": "official|featured|community|testing", 271 | "token": "…" 272 | } 273 | ``` 274 | Successful response: 275 | ```json 276 | { 277 | "changed": true 278 | } 279 | ``` 280 | 281 | API used by moderators to change the support level of an asset. 282 | 283 | ## Asset edits API 284 | 285 |
286 | 287 | ### `POST /asset`, `POST /asset/{id}`, `POST /asset/edit/{id}` 288 | ```json 289 | { 290 | "token": "…", 291 | 292 | "title": "Snake", 293 | "description": "Lorem ipsum…", 294 | "category_id": "1", 295 | "godot_version": "2.1", 296 | "version_string": "alpha", 297 | "cost": "GPLv3", 298 | "download_provider": "GitHub", 299 | "download_commit": "master", 300 | "browse_url": "https://github.com/…", 301 | "issues_url": "https://github.com/…/issues", 302 | "icon_url": "https://….png", 303 | "download_url": "https://github.com/…/archive/master.zip", 304 | "previews": [ 305 | { 306 | "enabled": true, 307 | "operation": "insert", 308 | "type": "image|video", 309 | "link": "…", 310 | "thumbnail": "…" 311 | }, 312 | { 313 | "enabled": true, 314 | "operation": "update", 315 | "edit_preview_id": "…", 316 | "type": "image|video", 317 | "link": "…", 318 | "thumbnail": "…" 319 | }, 320 | { 321 | "enabled": true, 322 | "operation": "delete", 323 | "edit_preview_id": "…" 324 | }, 325 | ] 326 | } 327 | ``` 328 | Successful result: 329 | ```json 330 | { 331 | "id": "(id of the asset edit)" 332 | } 333 | ``` 334 | 335 | Create a new edit or update an existing one. Fields are required when creating a new asset, and are optional otherwise. Same for previews -- required when creating a new preview, may be missing if updating one. 336 | 337 | Notes: 338 | * Not passing `"enabled": true` for previews will result in them not being included in the edit. This may be fixed in the future. 339 | * `version_string` is free-form text, but `major.minor` or `major.minor.patch` format is best. 340 | * Available download providers can be seen on the asset library fronted. 341 | 342 |
343 | 344 | ### `GET /asset/edit/{id}` 345 | No query params. 346 | Example result: 347 | ```json 348 | { 349 | "edit_id": "1", 350 | "asset_id": "1", 351 | "user_id": "1", 352 | "title": null, 353 | "description": null, 354 | "category_id": null, 355 | "godot_version": null, 356 | "version_string": null, 357 | "cost": null, 358 | "download_provider": null, 359 | "download_commit": null, 360 | "browse_url": "…", 361 | "issues_url": "…", 362 | "icon_url": null, 363 | "download_url": "…", 364 | "author": "test", 365 | "previews": [ 366 | { 367 | "operation": "insert", 368 | "edit_preview_id": "60", 369 | "preview_id": null, 370 | "type": "image", 371 | "link": "…", 372 | "thumbnail": "…", 373 | }, 374 | { 375 | "preview_id": "35", 376 | "type": "image", 377 | "link": "…", 378 | "thumbnail": "…" 379 | } 380 | ], 381 | "original": { 382 | … original asset fields … 383 | }, 384 | "status": "new", 385 | "reason": "", 386 | "warning": "…" 387 | } 388 | ``` 389 | 390 | Returns a previously-submitted asset edit. All fields with `null` are unchanged, and will stay the same as in the `original`. 391 | The `previews` array is merged from the new and original previews. 392 | 393 |
394 | 395 | ### `POST /asset/edit/{id}/review` 396 | ```json 397 | { 398 | "token": "…" 399 | } 400 | ``` 401 | Successful result: the asset edit, without the original asset. 402 | 403 | Moderator-only. Put an edit in review. It is impossible to change it after this. 404 | 405 |
406 | 407 | ### `POST /asset/edit/{id}/accept` 408 | ```json 409 | { 410 | "token": "…", 411 | } 412 | ``` 413 | Successful result: the asset edit, without the original asset. 414 | 415 | Moderator-only. Apply an edit previously put in review. 416 | 417 |
418 | 419 | ### `POST /asset/edit/{id}/reject` 420 | ```json 421 | { 422 | "token": "…", 423 | "reason": "…" 424 | } 425 | ``` 426 | Successful result: the asset edit, without the original asset. 427 | 428 | Moderator-only. Reject an edit previously put in review. 429 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Project maintainers: 2 | ==================== 3 | Alket Rexhepi (@alketii) 4 | Bojidar Marinov (@bojidar-bg) 5 | 6 | 7 | Contributors: 8 | ============= 9 | Rémi Verschelde (@akien-mga) 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Godot Asset Library 2 | 3 | First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to the Godot Engine's [official asset library web front end](https://godotengine.org/asset-library) and its REST API. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ### :warning: Two important notes before we dive into the details 8 | 9 | 1. Issues with the asset library front end included in the Godot Engine should be reported in the [Godot Engine repository](https://github.com/godotengine/godot/issues?q=is%3Aissue+is%3Aopen+asset+label%3Atopic%3Aassetlib). 10 | 2. Issues with individual assets should be reported in the separate issue trackers of the respective assets, usually linked on the asset pages under the "Submit an issue" button. 11 | 12 | ## Table Of Contents 13 | 14 | - [Code of Conduct](#code-of-conduct) 15 | 16 | - [I don't want to read this whole thing, I just have a question!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) 17 | 18 | - [What should I know before I get started?](#what-should-i-know-before-i-get-started) 19 | - [The Godot Asset Library](#the-godot-asset-library) 20 | 21 | - [How Can I Contribute?](#how-can-i-contribute) 22 | - [Reporting Bugs](#reporting-bugs-and-suggesting-enhancements) 23 | - [Code Contributions and Pull Requests](#code-contributions-and-pull-requests) 24 | 25 | ## Code of Conduct 26 | 27 | This project and everyone participating in it is governed by the [Godot Code of Conduct](https://godotengine.org/code-of-conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Godot's Code of Conduct team](mailto:conduct@godotengine.org). 28 | 29 | ## I don't want to read this whole thing I just have a question! 30 | 31 | > **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below. 32 | 33 | We have several forums, chats and groups where the community chimes in with helpful advice if you have questions: 34 | 35 | * [Questions & Answers](https://godotengine.org/qa/) — The official Godot message board 36 | * [Forum](https://godotforums.org/) — Community forum for all Godot developers 37 | 38 | Plus several groups at [Facebook](https://www.facebook.com/groups/godotengine/), [Reddit](https://www.reddit.com/r/godot), [Steam](https://steamcommunity.com/app/404790) and others. See [Godot Communities](https://godotengine.org/community) for a more complete list. And if chat is more your speed, you can join us at [IRC](http://webchat.freenode.net/?channels=#godotengine), [Discord](https://discord.gg/zH7NUgz) or [Matrix](https://matrix.to/#/#godotengine:matrix.org). 39 | 40 | ## What should I know before I get started? 41 | 42 | ### The Godot Asset Library 43 | 44 | The Godot Asset Library, otherwise known as the AssetLib, is a repository of user-submitted Godot addons, scripts, tools and other resources, collectively referred to as assets. They’re available to all Godot users for download directly from within the engine, but it can also be accessed at [Godot’s official website](https://godotengine.org/asset-library) via the PHP scripts and API functions maintained in this repository. 45 | 46 | Please note that the AssetLib is relatively young — it may have various pain points, bugs and usability issues. 47 | 48 | ## How can I contribute? 49 | 50 | ### Reporting Bugs and Suggesting Enhancements 51 | 52 | The golden rule is to **always open *one* issue for *one* bug**. If you notice 53 | several bugs and want to report them, make sure to create one new issue for 54 | each of them. 55 | 56 | If you're reporting a new bug, you'll make our life simpler (and the 57 | fix will come sooner) by following these guidelines: 58 | 59 | ### Search first in the existing database 60 | 61 | Issues are often reported several times by various users. It's good practice to 62 | **search first in the [issue tracker](https://github.com/godotengine/godot-asset-library/issues) 63 | before reporting your issue**. If you don't find a relevant match or if you're 64 | unsure, don't hesitate to **open a new issue**. The bugsquad will handle it 65 | from there if it's a duplicate. 66 | 67 | ### Code Contributions and Pull Requests 68 | 69 | If you want to add or improve asset library features, please make sure that: 70 | 71 | - This functionality is desired, which means that it solves a common use case 72 | that several users will need. 73 | - You talked to other developers on how to implement it best. 74 | - Even if it doesn't get merged, your PR is useful for future work by another 75 | developer. 76 | 77 | Similar rules can be applied when contributing bug fixes - it's always best to 78 | discuss the implementation in the bug report first if you are not 100% about 79 | what would be the best fix. 80 | 81 | The general [Godot Contributing docs](https://docs.godotengine.org/en/latest/community/contributing/index.html) 82 | also have important information on the PR workflow and the code style we use in this project. 83 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 The Godot Engine community 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot's Asset Library 2 | 3 | ___ 4 | 5 | **Note:** This asset library backend and frontend is now in maintenance mode. 6 | Feel free to submit bug fixes and small improvements, but please refrain from 7 | working on large features. In the future, the [Godot Foundation](https://godot.foundation/)'s asset store 8 | will deprecate this library. 9 | 10 | ___ 11 | 12 | REST API and frontend for Godot Engine's [official asset library](https://godotengine.org/asset-library). 13 | 14 | [Endpoints](./API.md) 15 | 16 | ## Installation 17 | 18 | Run the following commands to get a running installation of the project: 19 | 20 | ````bash 21 | composer install 22 | cp src/settings-local-example.php src/settings-local.php 23 | ```` 24 | 25 | Now you should proceed to update `src/settings-local.php` with your DB password and session secret. 26 | 27 | ## Browser support 28 | 29 | When working on new features, keep in mind this website only supports 30 | *evergreen browsers*: 31 | 32 | - Chrome (latest version and N-1 version) 33 | - Edge (latest version and N-1 version) 34 | - Firefox (latest version, N-1 version, and latest ESR version) 35 | - Opera (latest version and N-1 version) 36 | - Safari (latest version and N-1 version) 37 | 38 | Internet Explorer isn't supported. 39 | -------------------------------------------------------------------------------- /api/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | RewriteBase /asset-library/api 8 | 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | RewriteRule ^ index.php [QSA,L] 11 | -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | run(); 40 | -------------------------------------------------------------------------------- /assets/css/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --base-color-text: #333; 3 | --secondary-background-color: #f8f8f8; 4 | --highlight-background-color: #e7e7e7; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 9 | display: flex; 10 | flex-direction: column; 11 | min-height: 100vh; 12 | } 13 | 14 | main { 15 | flex-grow: 1; 16 | } 17 | 18 | footer { 19 | padding-bottom: 12px; 20 | } 21 | 22 | .logo-dark { 23 | display: none; 24 | } 25 | 26 | .form-search .form-group { 27 | margin-bottom: 0.2em; 28 | margin-right: 40px; 29 | } 30 | 31 | @media screen and (max-width: 768px) { 32 | .form-search .form-group { 33 | margin-right: 0; 34 | } 35 | } 36 | 37 | @supports (display: flex) { 38 | @media screen and (min-width: 768px) { 39 | .form-search { 40 | display: flex; 41 | justify-content: space-between; 42 | flex-flow: wrap; 43 | } 44 | .form-search .form-group { 45 | margin-right: 1em; 46 | margin-left: 1em; 47 | flex-shrink: 0; 48 | display: block; 49 | } 50 | } 51 | } 52 | 53 | .required_mark::after { 54 | content: ' *'; 55 | color: #d9534f; 56 | } 57 | 58 | .media-object { 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | flex-shrink: 0; 62 | } 63 | 64 | .nowrap { 65 | white-space: nowrap; 66 | display: inline-block; 67 | } 68 | 69 | .label { 70 | line-height: 1.8; 71 | } 72 | 73 | .asset-search-container { 74 | display: flex; 75 | flex-direction: row; 76 | gap: 24px; 77 | } 78 | 79 | @media (max-width: 768px) { 80 | .asset-search-container { 81 | flex-direction: column; 82 | } 83 | } 84 | 85 | .asset-search-container > form { 86 | display: flex; 87 | flex-direction: column; 88 | gap: 16px; 89 | width: 220px; 90 | min-width: 220px; 91 | } 92 | @media (max-width: 768px) { 93 | .asset-search-container > form { 94 | width: 100%; 95 | } 96 | } 97 | 98 | .asset-search-container > form > * + * { 99 | border-top: 1px solid black; 100 | padding-top: 16px; 101 | } 102 | 103 | .asset-search-container > form .form-actions > a { 104 | width: 100%; 105 | } 106 | 107 | .asset-search-container > form .form-search { 108 | display: flex; 109 | gap: 16px; 110 | flex-flow: column; 111 | } 112 | 113 | .asset-search-container > form .form-search .form-group { 114 | display: flex; 115 | flex-direction: column; 116 | gap: 6px; 117 | margin: 0; 118 | } 119 | 120 | .asset-search-container > form .form-search .form-group label:first-child { 121 | font-size: 106%; 122 | } 123 | 124 | .asset-search-container > form .form-search .form-checkbox { 125 | cursor: pointer; 126 | font-weight: 600; 127 | } 128 | 129 | .asset-search-container > form .form-search .form-checkbox input[type=checkbox] { 130 | vertical-align: text-top; 131 | } 132 | 133 | .asset-search-container > form .form-search select { 134 | cursor: pointer; 135 | } 136 | 137 | .asset-search-container > form .form-search .form-highlighted { 138 | border-radius: 4px; 139 | padding: 4px 8px; 140 | } 141 | 142 | .asset-search-container > form .form-search .form-highlighted > .form-checkbox { 143 | margin: 0; 144 | } 145 | 146 | .asset-search-results { 147 | display: flex; 148 | flex-direction: column; 149 | justify-content: flex-end; 150 | width: 100%; 151 | } 152 | 153 | .asset-list { 154 | display: grid; 155 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 156 | grid-gap: 12px; 157 | padding: 0; 158 | list-style: none; 159 | } 160 | 161 | .asset-list .asset-header { 162 | flex: 1; 163 | } 164 | 165 | .asset-item { 166 | display: flex; 167 | flex-direction: column; 168 | } 169 | 170 | .asset-header { 171 | display: flex; 172 | gap: 12px; 173 | padding: 12px 12px 8px 12px; 174 | background-color: var(--secondary-background-color); 175 | border: 1px solid var(--highlight-background-color); 176 | } 177 | 178 | .asset-header:focus { 179 | outline: 1px solid var(--base-color-text); 180 | outline-offset: -1px; 181 | box-shadow: none; 182 | } 183 | 184 | .asset-header:hover, 185 | .asset-header:focus, 186 | .asset-header:active { 187 | text-decoration: none; 188 | background-color: var(--highlight-background-color); 189 | } 190 | 191 | .asset-header:hover .asset-title h4, 192 | .asset-header:focus .asset-title h4, 193 | .asset-header:active .asset-title h4 { 194 | text-decoration: underline; 195 | } 196 | 197 | .asset-footer { 198 | display: flex; 199 | justify-content: space-between; 200 | align-items: center; 201 | padding: 8px 12px; 202 | background-color: var(--secondary-background-color); 203 | border: 1px solid var(--highlight-background-color); 204 | } 205 | 206 | .asset-title { 207 | display: flex; 208 | flex-direction: column; 209 | flex: 1; 210 | gap: 12px; 211 | } 212 | 213 | .asset-title h4 { 214 | margin: 0; 215 | font-weight: bold; 216 | color: var(--base-color-text); 217 | } 218 | 219 | .table-tags .label { 220 | display: block; 221 | width: 100%; 222 | padding: 0; 223 | font-size: 14px; 224 | text-align: center; 225 | } 226 | 227 | .asset-tags .label { 228 | padding: 0 6px; 229 | font-size: 13px; 230 | } 231 | 232 | .asset-tags-container { 233 | display: flex; 234 | justify-content: space-between; 235 | align-items: center; 236 | flex: 1; 237 | gap: 6px; 238 | } 239 | 240 | .asset-tags { 241 | display: flex; 242 | flex-wrap: wrap; 243 | gap: 6px; 244 | } 245 | 246 | .pagination-container { 247 | flex: 1; 248 | display: flex; 249 | justify-content: center; 250 | align-items: flex-end; 251 | text-align: center; 252 | } 253 | 254 | .pagination-stats { 255 | display: flex; 256 | justify-content: space-between; 257 | gap: 8px; 258 | } 259 | 260 | @media (max-width: 768px) { 261 | .pagination-stats { 262 | flex-direction: column; 263 | } 264 | } 265 | 266 | .table-bordered.table-edit > tbody > tr > th { 267 | width: 1%; 268 | white-space: nowrap; 269 | text-align: center; 270 | vertical-align: middle; 271 | } 272 | 273 | .table-bordered.table-edit img { 274 | max-width: 100%; 275 | } 276 | 277 | @media(prefers-color-scheme: dark) { 278 | :root { 279 | --base-color-text: hsla(200, 00%, 100%, 0.85); 280 | --link-color: #80B0DB; 281 | --link-underline-color: #94c2ef; 282 | --primary-background-color: #25282b; 283 | --secondary-background-color: #333639; 284 | --highlight-background-color: #505356; 285 | --input-background-color: #3b3e40; 286 | --success-background-color: #567a48; 287 | --danger-background-color: #9c3d3b; 288 | --info-background-color: #41788B; 289 | --notice-background-color: #515e86; 290 | --warning-background-color: #b5761b; 291 | --default-background-color: #696969; 292 | --base-box-shadow: 0 0 4px rgba(0, 0, 0, 0.4); 293 | } 294 | 295 | body { 296 | background-color: var(--primary-background-color); 297 | color: var(--base-color-text) 298 | } 299 | 300 | footer { 301 | padding-top: 40px; 302 | margin-top: 20px; 303 | background-color: var(--secondary-background-color); 304 | border: none; 305 | } 306 | 307 | footer hr, 308 | .logo-light { 309 | display: none; 310 | } 311 | 312 | .logo-dark { 313 | display: initial; 314 | } 315 | 316 | .navbar-default { 317 | background-color: var(--secondary-background-color); 318 | border: none; 319 | box-shadow: var(--base-box-shadow); 320 | } 321 | 322 | .navbar-default .navbar-brand, 323 | .navbar-default .navbar-nav > li > a { 324 | color: var(--base-color-text) 325 | } 326 | 327 | .navbar-default .navbar-nav > li > a:focus, 328 | .navbar-default .navbar-nav > li > a:hover, 329 | .navbar-default .navbar-brand:focus, 330 | .navbar-default .navbar-brand:hover { 331 | color: white; 332 | text-decoration: underline; 333 | } 334 | 335 | .navbar-default .navbar-nav > .active > a, 336 | .navbar-default .navbar-nav > .active > a:focus, 337 | .navbar-default .navbar-nav > .active > a:hover { 338 | background-color: var(--highlight-background-color); 339 | color: var(--base-color-text) 340 | } 341 | 342 | .form-control { 343 | background-color: var(--input-background-color); 344 | color: var(--base-color-text) 345 | } 346 | 347 | .form-control:focus { 348 | border-color: white; 349 | } 350 | 351 | .img-thumbnail { 352 | background: none; 353 | border: none; 354 | } 355 | 356 | .asset-search-container > form > * + * { 357 | border-top-color: white; 358 | } 359 | 360 | legend, 361 | h4 small, 362 | .help-block, 363 | .text-muted { 364 | color: var(--base-color-text) 365 | } 366 | 367 | a, 368 | .btn-link { 369 | color: var(--link-color); 370 | } 371 | 372 | a:focus, 373 | a:hover, 374 | .btn-link:focus, 375 | .btn-link:hover { 376 | color: var(--link-underline-color); 377 | } 378 | 379 | .pagination > li > a, 380 | .btn-default { 381 | background-color: var(--input-background-color); 382 | border-color: var(--base-color-text); 383 | color: var(--base-color-text); 384 | } 385 | 386 | .pagination > li > a:hover, 387 | .pagination > li > a:focus, 388 | .pagination > li > a:active, 389 | .btn-default:focus, 390 | .btn-default:hover, 391 | .btn-default:active { 392 | background-color: var(--highlight-background-color); 393 | color: var(--base-color-text); 394 | } 395 | 396 | .pagination > .disabled > a, 397 | .pagination > .active > a, 398 | .btn-default.active { 399 | color: white; 400 | pointer-events: none; 401 | z-index: -1 !important; 402 | } 403 | 404 | .pagination > .disabled > a { 405 | background-color: var(--highlight-background-color); 406 | border-color: var(--highlight-background-color); 407 | } 408 | 409 | .pagination > .active > a, 410 | .btn-default.active { 411 | background-color: var(--notice-background-color); 412 | border-color: var(--notice-background-color); 413 | } 414 | 415 | .btn-danger { 416 | background-color: var(--danger-background-color); 417 | border-color: var(--danger-background-color); 418 | } 419 | 420 | .btn-success { 421 | background-color: var(--success-background-color); 422 | border-color: var(--success-background-color); 423 | } 424 | 425 | .btn-warning { 426 | background-color: var(--warning-background-color); 427 | border-color: var(--warning-background-color); 428 | } 429 | 430 | .btn-primary { 431 | background-color: var(--notice-background-color); 432 | border-color: var(--notice-background-color); 433 | } 434 | 435 | .btn-primary:hover, 436 | .btn-primary:focus, 437 | .btn-primary:active { 438 | background-color: var(--info-background-color); 439 | border-color: var(--info-background-color); 440 | } 441 | 442 | code, 443 | .bg-info, 444 | .label-primary { 445 | background-color: var(--notice-background-color); 446 | color: white; 447 | } 448 | 449 | .panel { 450 | background-color: var(--secondary-background-color); 451 | border: none; 452 | } 453 | 454 | .panel-default > .panel-heading { 455 | background-color: var(--highlight-background-color); 456 | color: white; 457 | } 458 | 459 | .label-default { 460 | background-color: var(--default-background-color); 461 | } 462 | 463 | .label-info, 464 | .panel-info > .panel-heading, 465 | .table > tbody > tr > td.info { 466 | background-color: var(--info-background-color); 467 | color: white; 468 | } 469 | 470 | .label-danger, 471 | .panel-danger > .panel-heading, 472 | .table > tbody > tr > td.danger { 473 | background-color: var(--danger-background-color); 474 | color: white; 475 | } 476 | 477 | .label-success, 478 | .panel-success > .panel-heading, 479 | .table > tbody > tr > td.success { 480 | background-color: var(--success-background-color); 481 | color: white; 482 | } 483 | 484 | .table > tbody > tr > td.active { 485 | background-color: var(--primary-background-color); 486 | } 487 | 488 | .asset-header, 489 | .asset-footer { 490 | border-color: var(--primary-background-color) 491 | } 492 | 493 | .table-bordered { 494 | border: 2px solid var(--secondary-background-color); 495 | } 496 | 497 | .table-bordered > thead > tr > th, 498 | .table-bordered:not(.table-edit) > tbody > tr > td, 499 | .table-bordered:not(.table-edit) > tbody > tr > th { 500 | border: none; 501 | } 502 | 503 | .table-bordered thead > tr > th { 504 | border: none; 505 | border-bottom: 2px solid var(--default-background-color); 506 | } 507 | 508 | .table-striped > tbody > tr:nth-of-type(2n+1) { 509 | background-color: var(--secondary-background-color); 510 | } 511 | 512 | .table-bordered.table-edit > tbody > tr > td, 513 | .table-bordered.table-edit > tbody > tr > th { 514 | border-color: var(--secondary-background-color); 515 | } 516 | 517 | .table-bordered.table-edit > thead > tr > th:first-child { 518 | border-bottom: 1px solid var(--secondary-background-color); 519 | } 520 | 521 | .table-bordered.table-edit > tbody > tr > th { 522 | background-color: var(--primary-background-color); 523 | border-right: 2px solid var(--default-background-color); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-asset-library/1b9c58b9d824ad7c636e4e7d2785eade459def4d/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-asset-library/1b9c58b9d824ad7c636e4e7d2785eade459def4d/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-asset-library/1b9c58b9d824ad7c636e4e7d2785eade459def4d/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotengine/godot-asset-library/1b9c58b9d824ad7c636e4e7d2785eade459def4d/assets/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 3 | -------------------------------------------------------------------------------- /assets/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "^3.0", 4 | "dflydev/fig-cookies": "^1.0", 5 | "slim/csrf": "^0.7.0", 6 | "monolog/monolog": "^1.17", 7 | "slim/php-view": "^2.0", 8 | "phpmailer/phpmailer": "^6.8" 9 | }, 10 | 11 | "autoload": { 12 | "psr-4": { 13 | "Godot\\AssetLibrary\\": "src/" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/.htaccess: -------------------------------------------------------------------------------- 1 | Order allow,deny 2 | Deny from all -------------------------------------------------------------------------------- /data/data.sql: -------------------------------------------------------------------------------- 1 | SET NAMES utf8; 2 | SET time_zone = '+00:00'; 3 | SET foreign_key_checks = 0; 4 | SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; 5 | 6 | DROP TABLE IF EXISTS `as_assets`; 7 | CREATE TABLE `as_assets` ( 8 | `asset_id` int(11) NOT NULL AUTO_INCREMENT, 9 | `user_id` int(11) NOT NULL DEFAULT '0', 10 | `title` varchar(255) NOT NULL, 11 | `description` text NOT NULL, 12 | `category_id` int(11) NOT NULL DEFAULT '6', 13 | `godot_version` int(7) NOT NULL, 14 | `version` int(11) NOT NULL, 15 | `version_string` varchar(20) NOT NULL, 16 | `cost` varchar(25) NOT NULL DEFAULT 'GPLv3', 17 | `rating` int(11) NOT NULL DEFAULT '1', 18 | `support_level` tinyint(4) NOT NULL, 19 | `download_provider` tinyint(4) NOT NULL, 20 | `download_commit` varchar(2048) NOT NULL, 21 | `browse_url` varchar(1024) NOT NULL, 22 | `issues_url` varchar(1024) NOT NULL, 23 | `icon_url` varchar(1024) NOT NULL, 24 | `searchable` tinyint(1) NOT NULL DEFAULT '0', 25 | `modify_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 26 | PRIMARY KEY (`asset_id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 28 | 29 | DROP TABLE IF EXISTS `as_asset_edits`; 30 | CREATE TABLE `as_asset_edits` ( 31 | `edit_id` int(11) NOT NULL AUTO_INCREMENT, 32 | `asset_id` int(11) NOT NULL, 33 | `user_id` int(11) NOT NULL, 34 | `title` varchar(255) DEFAULT NULL, 35 | `description` text, 36 | `category_id` int(11) DEFAULT NULL, 37 | `godot_version` int(7) NOT NULL, 38 | `version_string` varchar(11) DEFAULT NULL, 39 | `cost` varchar(25) DEFAULT NULL, 40 | `download_provider` tinyint(4) DEFAULT NULL, 41 | `download_commit` varchar(2048) DEFAULT NULL, 42 | `browse_url` varchar(1024) DEFAULT NULL, 43 | `issues_url` varchar(1024) DEFAULT NULL, 44 | `icon_url` varchar(1024) DEFAULT NULL, 45 | `status` tinyint(4) NOT NULL DEFAULT '0', 46 | `reason` text NOT NULL, 47 | `submit_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 48 | `modify_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 49 | PRIMARY KEY (`edit_id`), 50 | KEY `asset_id` (`asset_id`), 51 | KEY `status` (`status`) 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 53 | DROP TABLE IF EXISTS `as_asset_edit_previews`; 54 | CREATE TABLE `as_asset_edit_previews` ( 55 | `edit_preview_id` int(11) NOT NULL AUTO_INCREMENT, 56 | `edit_id` int(11) NOT NULL, 57 | `preview_id` int(11) NOT NULL, 58 | `type` enum('image','video') DEFAULT NULL, 59 | `link` varchar(1024) DEFAULT NULL, 60 | `thumbnail` varchar(1024) DEFAULT NULL, 61 | `operation` tinyint(4) NOT NULL, 62 | PRIMARY KEY (`edit_preview_id`), 63 | KEY `asset_id` (`edit_id`), 64 | KEY `type` (`type`), 65 | KEY `preview_id` (`preview_id`) 66 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 67 | 68 | 69 | DROP TABLE IF EXISTS `as_asset_previews`; 70 | CREATE TABLE `as_asset_previews` ( 71 | `preview_id` int(11) NOT NULL AUTO_INCREMENT, 72 | `asset_id` int(11) NOT NULL, 73 | `type` enum('image','video') NOT NULL, 74 | `link` varchar(1024) NOT NULL, 75 | `thumbnail` varchar(1024) NOT NULL, 76 | PRIMARY KEY (`preview_id`), 77 | KEY `asset_id` (`asset_id`), 78 | KEY `type` (`type`) 79 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 80 | 81 | 82 | DROP TABLE IF EXISTS `as_categories`; 83 | CREATE TABLE `as_categories` ( 84 | `category_id` int(11) NOT NULL AUTO_INCREMENT, 85 | `category` varchar(25) NOT NULL, 86 | `category_type` tinyint(4) NOT NULL, 87 | PRIMARY KEY (`category_id`), 88 | UNIQUE KEY `name` (`category`) 89 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 90 | 91 | INSERT INTO `as_categories` (`category_id`, `category`, `category_type`) VALUES 92 | (1, '2D Tools', 0), 93 | (2, '3D Tools', 0), 94 | (3, 'Shaders', 0), 95 | (4, 'Materials', 0), 96 | (5, 'Tools', 0), 97 | (6, 'Scripts', 0), 98 | (7, 'Misc', 0), 99 | (8, 'Templates', 1), 100 | (9, 'Projects', 1), 101 | (10, 'Demos', 1); 102 | 103 | DROP TABLE IF EXISTS `as_users`; 104 | CREATE TABLE `as_users` ( 105 | `user_id` int(11) NOT NULL AUTO_INCREMENT, 106 | `username` varchar(100) NOT NULL, 107 | `email` varchar(1024) NOT NULL, 108 | `password_hash` varchar(64) NOT NULL, 109 | `type` tinyint(1) NOT NULL DEFAULT '0', 110 | `session_token` varbinary(24) DEFAULT NULL, 111 | `reset_token` binary(24) DEFAULT NULL, 112 | PRIMARY KEY (`user_id`), 113 | UNIQUE KEY `username` (`username`), 114 | UNIQUE KEY `session_token` (`session_token`), 115 | KEY `reset_token` (`reset_token`) 116 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 117 | 118 | -- add indexes 119 | ALTER TABLE `as_assets` ADD INDEX `godot_version_index` (`godot_version`); 120 | ALTER TABLE `as_asset_edits` ADD INDEX `godot_version_index` (`godot_version`); 121 | ALTER TABLE `as_users` ADD INDEX (`reset_token`); 122 | -------------------------------------------------------------------------------- /data/migration-1.sql: -------------------------------------------------------------------------------- 1 | -- Adminer 4.2.5 MySQL dump 2 | 3 | SET NAMES utf8; 4 | SET time_zone = '+00:00'; 5 | SET foreign_key_checks = 0; 6 | SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; 7 | 8 | DROP TABLE IF EXISTS `as_assets`; 9 | CREATE TABLE `as_assets` ( 10 | `asset_id` int(11) NOT NULL AUTO_INCREMENT, 11 | `user_id` int(11) NOT NULL DEFAULT '0', 12 | `title` varchar(255) NOT NULL, 13 | `description` text NOT NULL, 14 | `category_id` int(11) NOT NULL DEFAULT '6', 15 | `version` int(11) NOT NULL, 16 | `version_string` varchar(20) NOT NULL, 17 | `cost` varchar(25) NOT NULL DEFAULT 'GPLv3', 18 | `rating` int(11) NOT NULL DEFAULT '1', 19 | `support_level` tinyint(4) NOT NULL, 20 | `download_url` varchar(1024) NOT NULL, 21 | `download_hash` text NOT NULL, 22 | `browse_url` varchar(1024) NOT NULL, 23 | `icon_url` varchar(1024) NOT NULL, 24 | `searchable` tinyint(1) NOT NULL DEFAULT '0', 25 | `modify_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 26 | PRIMARY KEY (`asset_id`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 28 | 29 | 30 | DROP TABLE IF EXISTS `as_asset_edits`; 31 | CREATE TABLE `as_asset_edits` ( 32 | `edit_id` int(11) NOT NULL AUTO_INCREMENT, 33 | `asset_id` int(11) NOT NULL, 34 | `user_id` int(11) NOT NULL, 35 | `title` varchar(255) DEFAULT NULL, 36 | `description` text, 37 | `category_id` int(11) DEFAULT NULL, 38 | `version_string` varchar(11) DEFAULT NULL, 39 | `cost` varchar(25) DEFAULT NULL, 40 | `download_url` varchar(1024) DEFAULT NULL, 41 | `browse_url` varchar(1024) DEFAULT NULL, 42 | `icon_url` varchar(1024) DEFAULT NULL, 43 | `status` tinyint(4) NOT NULL DEFAULT '0', 44 | `reason` text NOT NULL, 45 | `submit_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 46 | `modify_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 47 | PRIMARY KEY (`edit_id`), 48 | KEY `asset_id` (`asset_id`), 49 | KEY `status` (`status`) 50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 51 | 52 | 53 | DROP TABLE IF EXISTS `as_asset_edit_previews`; 54 | CREATE TABLE `as_asset_edit_previews` ( 55 | `edit_preview_id` int(11) NOT NULL AUTO_INCREMENT, 56 | `edit_id` int(11) NOT NULL, 57 | `preview_id` int(11) NOT NULL, 58 | `type` enum('image','video') DEFAULT NULL, 59 | `link` varchar(1024) DEFAULT NULL, 60 | `thumbnail` varchar(1024) DEFAULT NULL, 61 | `operation` tinyint(4) NOT NULL, 62 | PRIMARY KEY (`edit_preview_id`), 63 | KEY `asset_id` (`edit_id`), 64 | KEY `type` (`type`), 65 | KEY `preview_id` (`preview_id`) 66 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 67 | 68 | 69 | DROP TABLE IF EXISTS `as_asset_previews`; 70 | CREATE TABLE `as_asset_previews` ( 71 | `preview_id` int(11) NOT NULL AUTO_INCREMENT, 72 | `asset_id` int(11) NOT NULL, 73 | `type` enum('image','video') NOT NULL, 74 | `link` varchar(1024) NOT NULL, 75 | `thumbnail` varchar(1024) NOT NULL, 76 | PRIMARY KEY (`preview_id`), 77 | KEY `asset_id` (`asset_id`), 78 | KEY `type` (`type`) 79 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 80 | 81 | 82 | DROP TABLE IF EXISTS `as_categories`; 83 | CREATE TABLE `as_categories` ( 84 | `category_id` int(11) NOT NULL AUTO_INCREMENT, 85 | `category` varchar(25) NOT NULL, 86 | `category_type` tinyint(4) NOT NULL, 87 | PRIMARY KEY (`category_id`), 88 | UNIQUE KEY `name` (`category`) 89 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 90 | 91 | INSERT INTO `as_categories` (`category_id`, `category`, `category_type`) VALUES 92 | (1, '2D Tools', 0), 93 | (2, '3D Tools', 0), 94 | (3, 'Shaders', 0), 95 | (4, 'Materials', 0), 96 | (5, 'Tools', 0), 97 | (6, 'Scripts', 0), 98 | (7, 'Misc', 0), 99 | (8, 'Templates', 1), 100 | (9, 'Projects', 1), 101 | (10, 'Demos', 1); 102 | 103 | DROP TABLE IF EXISTS `as_users`; 104 | CREATE TABLE `as_users` ( 105 | `user_id` int(11) NOT NULL AUTO_INCREMENT, 106 | `username` varchar(100) NOT NULL, 107 | `email` varchar(1024) NOT NULL, 108 | `password_hash` varchar(64) NOT NULL, 109 | `type` tinyint(1) NOT NULL DEFAULT '0', 110 | `session_token` varbinary(24) DEFAULT NULL, 111 | PRIMARY KEY (`user_id`), 112 | UNIQUE KEY `username` (`username`), 113 | UNIQUE KEY `session_token` (`session_token`) 114 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 115 | 116 | 117 | -- 2016-07-25 18:15:07 118 | -------------------------------------------------------------------------------- /data/migration-2.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | ALTER TABLE `as_assets` ADD COLUMN `download_provider` TINYINT NOT NULL AFTER `download_url`; 4 | ALTER TABLE `as_assets` ADD `download_commit` VARCHAR(64) NOT NULL AFTER `download_provider`; 5 | 6 | UPDATE `as_assets` SET `download_provider`=0,`download_commit`= 7 | SUBSTRING(`download_url`, 8 | LOCATE('/',`download_url`,LOCATE('/',`download_url`,LOCATE('/',`download_url`,20)+1)+1)+1, 9 | -- Matching the last slash, which is V right below the `V` 10 | -- https://github.com/.../.../archive/....zip 11 | -- ^ This slash is the 19-th character 12 | LENGTH(`download_url`) - LOCATE('/',`download_url`,LOCATE('/',`download_url`,LOCATE('/',`download_url`,20)+1)+1) - 4 13 | -- Repeating locate formula :/ 14 | ) WHERE `download_url` RLIKE 'https:\/\/github.com\/[^\/]+\/[^\/]+\/archive\/[^\/]+.zip'; 15 | 16 | ALTER TABLE `as_assets` DROP COLUMN `download_url`; 17 | 18 | ALTER TABLE `as_asset_edits` ADD COLUMN `download_provider` TINYINT NULL AFTER `download_url`; 19 | ALTER TABLE `as_asset_edits` ADD `download_commit` VARCHAR(64) NULL AFTER `download_provider`; 20 | 21 | UPDATE `as_asset_edits` SET `download_provider`=0,`download_commit`= 22 | SUBSTRING(`download_url`, 23 | LOCATE('/',`download_url`,LOCATE('/',`download_url`,LOCATE('/',`download_url`,20)+1)+1)+1, 24 | -- Matching the last slash, which is V right below the `V` 25 | -- https://github.com/.../.../archive/....zip 26 | -- ^ This slash is the 19-th character 27 | LENGTH(`download_url`) - LOCATE('/',`download_url`,LOCATE('/',`download_url`,LOCATE('/',`download_url`,20)+1)+1) - 4 28 | -- Repeating locate formula :/ 29 | ) WHERE `download_url` RLIKE 'https:\/\/github.com\/[^\/]+\/[^\/]+\/archive\/[^\/]+.zip'; 30 | 31 | ALTER TABLE `as_asset_edits` DROP COLUMN `download_url`; 32 | -------------------------------------------------------------------------------- /data/migration-3.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `as_assets` ADD `issues_url` VARCHAR(1024) NOT NULL AFTER `browse_url`; 3 | ALTER TABLE `as_asset_edits` ADD `issues_url` VARCHAR(1024) NULL DEFAULT NULL AFTER `browse_url`; -------------------------------------------------------------------------------- /data/migration-4.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `as_users` ADD `reset_token` BINARY(32) NULL DEFAULT NULL AFTER `session_token`, ADD INDEX (`reset_token`); 3 | ALTER TABLE `as_users` CHANGE `session_token` `session_token` BINARY(24) NULL DEFAULT NULL; 4 | -------------------------------------------------------------------------------- /data/migration-5.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `as_assets` ADD `godot_version` INT(7) NOT NULL AFTER `category_id`, ADD INDEX `godot_version_index` (`godot_version`); 3 | UPDATE `as_assets` SET `godot_version` = 20100 WHERE `godot_version` = 0; 4 | 5 | ALTER TABLE `as_asset_edits` ADD `godot_version` INT(7) NULL AFTER `category_id`, ADD INDEX `godot_version_index` (`godot_version`); 6 | UPDATE `as_asset_edits` SET `godot_version` = 20100 WHERE `asset_id` = -1; 7 | -------------------------------------------------------------------------------- /data/migration-6.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `as_assets` CHANGE `download_commit` `download_commit` varchar(2048) NOT NULL; 3 | 4 | ALTER TABLE `as_asset_edits` CHANGE `download_commit` `download_commit` varchar(2048) NULL; 5 | -------------------------------------------------------------------------------- /data/migration-7.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `as_assets` DROP `download_hash`; 3 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | &|& 3 | 4 | namespace Godot\AssetLibrary\Helpers; 5 | 6 | class Tokens 7 | { 8 | private $c; 9 | 10 | public function __construct($c) 11 | { 12 | $this->c = $c; 13 | } 14 | 15 | private function signToken($token) 16 | { 17 | return hash_hmac('sha256', $token, $this->c->settings['auth']['secret'], true); 18 | } 19 | 20 | public function generate($data) 21 | { 22 | $token_data = json_encode($data); 23 | 24 | $token_id = openssl_random_pseudo_bytes(8); 25 | $token_time = time(); 26 | 27 | $token_payload = base64_encode($token_data) . '&' . base64_encode($token_id) . '|' . base64_encode($token_time); 28 | $token = $token_payload . '&' . base64_encode($this->signToken($token_payload)); 29 | 30 | return $token; 31 | } 32 | 33 | public function validate($token) 34 | { 35 | $token_parts = explode('&', $token); 36 | if (count($token_parts) != 3) { 37 | return false; 38 | } 39 | 40 | $token_data = json_decode(base64_decode($token_parts[0])); 41 | $token_time = base64_decode(explode('|', $token_parts[1])[1]); 42 | $token_signature = base64_decode($token_parts[2]); 43 | 44 | $token_payload = $token_parts[0] . '&' . $token_parts[1]; 45 | 46 | if ($token_signature !== $this->signToken($token_payload) || time() > $token_time + $this->c->settings['auth']['tokenExpirationTime']) { 47 | return false; 48 | } 49 | 50 | return $token_data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Helpers/Utils.php: -------------------------------------------------------------------------------- 1 | c = $c; 12 | } 13 | 14 | public function getComputedDownloadUrl($repo_url, $provider, $commit, &$warning=null, &$light_warning=null) // i.e. browse_url, download_provider, download_commit 15 | { 16 | $repo_url = rtrim($repo_url, '/'); 17 | if (is_int($provider)) { 18 | $provider = $this->c->constants['download_provider'][$provider]; 19 | } 20 | $warning_suffix = "Please, ensure that the URL and the repository provider are correct."; 21 | $light_warning_suffix = "Please, doublecheck that the URL and the repository provider are correct."; 22 | if (sizeof(preg_grep('/^https:\/\/.+\.git$/i', [$repo_url])) != 0) { 23 | $warning[] = "\"$repo_url\" doesn't look correct; it probably shouldn't end in .git. $warning_suffix"; 24 | } 25 | if ($provider != 'Custom') { 26 | // Git commits are either 40 (SHA1) or 64 (SHA2) hex characters 27 | if (sizeof(preg_grep('/^[a-f0-9]{40}([a-f0-9]{24})?$/', [$commit])) == 0) { 28 | $warning[] = "Using Git tags or branches is no longer supported. Please give a full Git commit hash instead, or use the Custom download provider for GitHub Releases downloads.\n"; 29 | } 30 | } 31 | switch ($provider) { 32 | case 'GitHub': 33 | if (sizeof(preg_grep('/^https:\/\/github\.com\/[^\/]+?\/[^\/]+?$/i', [$repo_url])) == 0) { 34 | $warning[] = "\"$repo_url\" doesn't look correct; it should be similar to \"https://github.com//\". $warning_suffix"; 35 | } 36 | return "$repo_url/archive/$commit.zip"; 37 | case 'GitLab': 38 | if (sizeof(preg_grep('/^https:\/\/(gitlab\.com|[^\/]+)\/[^\/]+?\/[^\/]+?(?:[^\/]+\/?)*$/i', [$repo_url])) == 0) { 39 | $warning[] = "\"$repo_url\" doesn't look correct; it should be similar to \"https://///\". $warning_suffix"; 40 | } elseif (sizeof(preg_grep('/^https:\/\/(gitlab\.com)\/[^\/]+?\/[^\/]+?(?:[^\/]+\/?)*$/i', [$repo_url])) == 0) { 41 | $light_warning[] = "\"$repo_url\" might not be correct; it should be similar to \"https://gitlab.com///\", unless the asset is hosted on a custom instance of GitLab. $light_warning_suffix"; 42 | } 43 | return "$repo_url/-/archive/$commit.zip"; 44 | case 'BitBucket': 45 | if (sizeof(preg_grep('/^https:\/\/bitbucket\.org\/[^\/]+?\/[^\/]+?$/i', [$repo_url])) == 0) { 46 | $warning[] = "\"$repo_url\" doesn't look correct; it should be similar to \"https://bitbucket.org//\". $warning_suffix"; 47 | } 48 | return "$repo_url/get/$commit.zip"; 49 | case 'Gogs/Gitea/Codeberg': 50 | if (sizeof(preg_grep('/^https?:\/\/[^\/]+?\/[^\/]+?\/[^\/]+?$/i', [$repo_url])) == 0) { 51 | $warning[] = "\"$repo_url\" doesn't look correct; it should be similar to \"http:////\". $warning_suffix"; 52 | } 53 | if (sizeof(preg_grep('/^https:\/\/(notabug\.org|codeberg\.org)\/[^\/]+?\/[^\/]+?$/i', [$repo_url])) == 0) { 54 | $light_warning[] = "Since Gogs/Gitea/Codeberg might be self-hosted, we can't be sure that \"$repo_url\" is a valid repository URL. $light_warning_suffix"; 55 | } 56 | return "$repo_url/archive/$commit.zip"; 57 | case 'cgit': 58 | if (sizeof(preg_grep('/^https?:\/\/[^\/]+?\/[^\/]+?\/[^\/]+?$/i', [$repo_url])) == 0) { 59 | $warning[] = "\"$repo_url\" doesn't look correct; it should be similar to \"http:////\". $warning_suffix"; 60 | } 61 | $light_warning[] = "Since cgit might be self-hosted, we can't be sure that \"$repo_url\" is a valid cgit URL. $light_warning_suffix"; 62 | return "$repo_url/snapshot/$commit.zip"; 63 | case 'Custom': 64 | if (sizeof(preg_grep('/^https?:\/\/.+?\.zip$/i', [$commit])) == 0) { 65 | $warning[] = "\"$commit\" doesn't look correct; it should be similar to \"http://.zip\". $warning_suffix"; 66 | } 67 | return "$commit"; 68 | default: 69 | return "$repo_url/$commit.zip"; // Obviously incorrect, but we would like to have some default case... 70 | } 71 | } 72 | 73 | public function getDefaultIssuesUrl($repo_url, $provider) // i.e. browse_url, download_provider 74 | { 75 | $repo_url = rtrim($repo_url, '/'); 76 | if (is_int($provider)) { 77 | $provider = $this->c->constants['download_provider'][$provider]; 78 | } 79 | switch ($provider) { 80 | case 'GitHub': 81 | case 'GitLab': 82 | case 'BitBucket': 83 | case 'Gogs/Gitea/Codeberg': 84 | return "$repo_url/issues"; 85 | case 'cgit': 86 | default: 87 | return ""; 88 | } 89 | } 90 | 91 | public function getFormattedGodotVersion($internal_id, &$warning=null, &$light_warning=null) 92 | { 93 | if ($internal_id == $this->c->constants['special_godot_versions']['unknown']) { 94 | $light_warning[] = "Setting Godot version as \"unknown\" is not recommended, as it would prevent people from finding your asset easily."; 95 | } 96 | if (isset($this->c->constants['special_godot_versions'][$internal_id])) { 97 | return $this->c->constants['special_godot_versions'][$internal_id]; 98 | } else { 99 | $major = floor($internal_id / 10000) % 100; 100 | $minor = floor($internal_id / 100) % 100; 101 | $patch = floor($internal_id / 1) % 100; 102 | if ($patch != 0) { 103 | return $major . '.' . $minor . '.' . $patch; 104 | } else { 105 | return $major . '.' . $minor; 106 | } 107 | } 108 | } 109 | 110 | public function getUnformattedGodotVersion($value) 111 | { 112 | if (is_int($value)) { 113 | return $value; 114 | } 115 | if (isset($this->c->constants['special_godot_versions'][$value])) { 116 | return $this->c->constants['special_godot_versions'][$value]; 117 | } else { 118 | $slices = explode('.', $value); 119 | $major = (int) $slices[0]; 120 | $minor = min(100, max(0, (int) (isset($slices[1]) ? $slices[1] : 0))); 121 | $patch = min(100, max(0, (int) (isset($slices[2]) ? $slices[2] : 0))); 122 | return $major * 10000 + $minor * 100 + $patch; 123 | } 124 | } 125 | 126 | public function errorResponseIfNotUserHasLevel($currentStatus, &$response, $user, $required_level_name, $message = 'You are not authorized to do this') 127 | { 128 | if ($user === false || $currentStatus) { 129 | return true; 130 | } 131 | 132 | if ((int) $user['type'] < $this->c->constants['user_type'][$required_level_name]) { 133 | $response = $response->withJson([ 134 | 'error' => $message, 135 | ], 403); 136 | return true; 137 | } 138 | return false; 139 | } 140 | 141 | public function errorResponseIfNotOwnerOrLevel($currentStatus, &$response, $user, $asset_id, $required_level_name, $message = 'You are not authorized to do this') 142 | { 143 | if($user === false || $currentStatus) { 144 | return true; 145 | } 146 | 147 | $query = $this->c->queries['asset']['get_one']; 148 | $query->bindValue(':id', (int) $asset_id, \PDO::PARAM_INT); 149 | $query->execute(); 150 | 151 | if($query->rowCount() <= 0) { 152 | return $response->withJson(['error' => 'Couldn\'t find asset with id '.$asset_id.'!'], 404); 153 | } 154 | 155 | $asset = $query->fetch(); 156 | 157 | if($asset['author_id'] != $user['user_id']) { 158 | return $this->errorResponseIfNotUserHasLevel(false, $response, $user, $required_level_name, $message); 159 | } 160 | 161 | return false; 162 | } 163 | 164 | public function errorResponseIfMissingOrNotString($currentStatus, &$response, $object, $property) 165 | { 166 | if ($currentStatus) { 167 | return true; 168 | } 169 | 170 | if (!isset($object[$property]) || !is_string($object[$property]) || $object[$property] == "") { 171 | $response = $response->withJson([ 172 | 'error' => $property . ' is required, and must be a string' 173 | ], 400); 174 | return true; 175 | } 176 | return false; 177 | } 178 | 179 | public function errorResponseIfQueryBad($currentStatus, &$response, $query, $message = 'An error occured while executing DB queries') 180 | { 181 | if ($currentStatus) { 182 | return true; 183 | } 184 | 185 | if ($query->errorCode() != '00000') { 186 | $this->c->logger->error('DBError', $query->errorInfo()); 187 | $response = $response->withJson([ 188 | 'error' => $message, 189 | ], 500); 190 | return true; 191 | } 192 | return false; 193 | } 194 | 195 | public function errorResponseIfQueryNoResults($currentStatus, &$response, $query, $message = 'DB returned no results') 196 | { 197 | if ($currentStatus) { 198 | return true; 199 | } 200 | 201 | if ($query->rowCount() == 0) { 202 | $response = $response->withJson([ 203 | 'error' => $message 204 | ], 404); 205 | return true; 206 | } 207 | return false; 208 | } 209 | 210 | public function ensureLoggedIn($currentStatus, &$response, $body, &$user, &$token_data = null, $reset = false) 211 | { 212 | $currentStatus = $this->errorResponseIfMissingOrNotString($currentStatus, $response, $body, 'token'); 213 | if ($currentStatus) { 214 | return true; 215 | } 216 | 217 | $token_data = $this->c->tokens->validate($body['token']); 218 | $error = $this->getUserFromTokenData(false, $response, $token_data, $user, $reset); 219 | return $error; 220 | } 221 | 222 | public function getUserFromTokenData($currentStatus, &$response, $token_data, &$user, $reset = false) 223 | { 224 | if ($currentStatus) { 225 | return true; 226 | } 227 | if (!$token_data) { 228 | $response = $response->withJson([ 229 | 'error' => 'Invalid token' 230 | ], 403); 231 | return true; 232 | } 233 | 234 | // Insecure 235 | // if(isset($token_data->user_id)) { 236 | // $query = $this->c->queries['user']['get_one']; 237 | // $query->bindValue(':id', (int) $token_data->user_id, PDO::PARAM_INT); 238 | // } 239 | if (isset($token_data->session) && !$reset) { 240 | $query = $this->c->queries['user']['get_by_session_token']; 241 | $query->bindValue(":session_token", base64_decode($token_data->session)); 242 | } elseif (isset($token_data->reset) && $reset) { 243 | $query = $this->c->queries['user']['get_by_reset_token']; 244 | $query->bindValue(":reset_token", base64_decode($token_data->reset)); 245 | } else { 246 | $response = $response->withJson([ 247 | 'error' => 'Invalid token' 248 | ], 403); 249 | return true; 250 | } 251 | 252 | $query->execute(); 253 | 254 | $currentStatus = $this->errorResponseIfQueryBad(false, $response, $query); 255 | $currentStatus = $this->errorResponseIfQueryNoResults($currentStatus, $response, $query, 'Nonexistent token submitted'); 256 | if ($currentStatus) { 257 | return true; 258 | } 259 | 260 | $user = $query->fetchAll()[0]; 261 | return false; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/constants.php: -------------------------------------------------------------------------------- 1 | $value) { 6 | $array[(string) $value] = $key; 7 | $array[$value] = $key; 8 | } 9 | return $array; 10 | } 11 | 12 | return $constants = [ 13 | 'edit_status' => double_map([ 14 | 'new' => 0, 15 | 'in_review' => 1, 16 | 'accepted' => 2, 17 | 'rejected' => 3, 18 | ]), 19 | 'edit_preview_operation' => double_map([ 20 | 'insert' => 0, 21 | 'remove' => 1, 22 | 'update' => 2, 23 | ]), 24 | 'category_type' => double_map([ 25 | 'addon' => '0', 26 | 'project' => '1', 27 | 'any' => '%', 28 | ]), 29 | 'support_level' => double_map([ 30 | 'testing' => 0, 31 | 'community' => 1, 32 | 'official' => 2, 33 | 'featured' => 2, 34 | ]), 35 | 'user_type' => double_map([ 36 | 'normal' => 0, 37 | 'verified' => 5, 38 | 'editor' => 25, 39 | 'moderator' => 50, 40 | 'admin' => 100, 41 | ]), 42 | 'download_provider' => double_map([ 43 | 'Custom' => -1, 44 | 'GitHub' => 0, 45 | 'GitLab' => 1, 46 | 'BitBucket' => 2, 47 | 'Gogs/Gitea/Codeberg' => 3, 48 | 'cgit' => 4, 49 | ]), 50 | 'asset_edit_fields' => [ 51 | 'title', 'description', 'category_id', 'godot_version', 52 | 'version_string', 'cost', 53 | 'download_provider', 'download_commit', 'browse_url', 'issues_url', 'icon_url', 54 | ], 55 | 'asset_edit_preview_fields' => [ 56 | 'type', 'link', 'thumbnail', 57 | ], 58 | 'special_godot_versions' => double_map([ 59 | 0 => 'unknown', 60 | 9999999 => 'custom_build' 61 | ]), 62 | 'common_godot_versions' => [ 63 | '2.0', 64 | '2.1', 65 | '2.2', 66 | '3.0', 67 | '3.1', 68 | '3.2', 69 | '3.3', 70 | '3.4', 71 | '3.5', 72 | '3.6', 73 | '4.0', 74 | '4.1', 75 | '4.2', 76 | '4.3', 77 | '4.4', 78 | 'unknown', 79 | 'custom_build', 80 | ], 81 | 'licenses' => [ 82 | 'MIT' => 'MIT', 83 | 'MPL-2.0' => 'MPL-2.0', 84 | 'GPLv3' => 'GPL v3', 85 | 'GPLv2' => 'GPL v2', 86 | 'LGPLv3' => 'LGPL v3', 87 | 'LGPLv2.1' => 'LGPL v2.1', 88 | 'LGPLv2' => 'LGPL v2', 89 | 'AGPLv3' => 'AGPL v3', 90 | 'EUPL-1.2' => 'European Union Public License 1.2', 91 | 'Apache-2.0' => 'Apache 2.0', 92 | 'CC0' => 'CC0 1.0 Universal', 93 | 'CC-BY-4.0' => 'CC BY 4.0 International', 94 | 'CC-BY-3.0' => 'CC BY 3.0 Unported', 95 | 'CC-BY-SA-4.0' => 'CC BY-SA 4.0 International', 96 | 'CC-BY-SA-3.0' => 'CC BY-SA 3.0 Unported', 97 | 'BSD-2-Clause' => 'BSD 2-clause License', 98 | 'BSD-3-Clause' => 'BSD 3-clause License', 99 | 'BSL-1.0' => 'Boost Software License', 100 | 'ISC' => 'ISC License', 101 | 'Unlicense' => 'The Unlicense License', 102 | 'Proprietary' => 'Proprietary (see LICENSE file)', 103 | ] 104 | ]; 105 | -------------------------------------------------------------------------------- /src/dependencies.php: -------------------------------------------------------------------------------- 1 | getContainer(); 5 | 6 | // view renderer 7 | $container['renderer'] = function ($c) { 8 | $settings = $c->get('settings')['renderer']; 9 | return new Slim\Views\PhpRenderer($settings['template_path']); 10 | }; 11 | 12 | // monolog 13 | $container['logger'] = function ($c) { 14 | $settings = $c->get('settings')['logger']; 15 | $logger = new Monolog\Logger($settings['name']); 16 | $logger->pushProcessor(new Monolog\Processor\UidProcessor()); 17 | $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], Monolog\Logger::DEBUG)); 18 | return $logger; 19 | }; 20 | 21 | // pdo 22 | $container['db'] = function ($c) { 23 | $settings = $c->get('settings')['db']; 24 | $db = new PDO($settings['dsn'], $settings['user'], $settings['pass']); 25 | $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); 26 | return $db; 27 | }; 28 | 29 | // constants 30 | $container['constants'] = function ($c) { 31 | return require_once __DIR__ . '/constants.php'; 32 | }; 33 | 34 | // queries 35 | $container['queries'] = function ($c) { 36 | $db = $c->db; 37 | 38 | $raw_queries = require_once __DIR__ . '/queries.php'; 39 | $queries = []; 40 | foreach ($raw_queries as $model => $model_queries) { 41 | $queries[$model] = []; 42 | foreach ($model_queries as $query_name => $query) { 43 | $queries[$model][$query_name] = $db->prepare($query); 44 | } 45 | } 46 | return $queries; 47 | }; 48 | 49 | // mail 50 | $container['mail'] = function ($c) { 51 | return function () use ($c) { 52 | $settings = $c->get('settings')['mail']; 53 | $mail = new PHPMailer\PHPMailer\PHPMailer; 54 | $mail->setFrom($settings['from']); 55 | if (isset($settings['replyTo'])) { 56 | $mail->addReplyTo($settings['replyTo']); 57 | } 58 | if (isset($settings['smtp'])) { 59 | $mail->isSMTP(); 60 | $mail->Host = $settings['smtp']['host']; 61 | $mail->Port = $settings['smtp']['port']; 62 | if (isset($settings['smtp']['auth'])) { 63 | $mail->SMTPAuth = true; 64 | $mail->Username = $settings['smtp']['auth']['user']; 65 | $mail->Password = $settings['smtp']['auth']['pass']; 66 | if ($settings['smtp']['secure']) { 67 | $mail->SMTPSecure = $settings['smtp']['secure']; 68 | } 69 | } else { 70 | $mail->SMTPAuth = true; 71 | } 72 | } 73 | return $mail; 74 | }; 75 | }; 76 | 77 | // csrf guard 78 | $container['csrf'] = function ($c) { 79 | session_name('assetlib-csrf'); 80 | session_start(); 81 | return new \Slim\Csrf\Guard; 82 | }; 83 | 84 | // cookies 85 | $container['cookies'] = function ($c) { 86 | return [ 87 | 'cookie' => function ($name, $value) { 88 | return Dflydev\FigCookies\Cookie::create($name, $value); 89 | }, 90 | 'setCookie' => function ($name) { 91 | return Dflydev\FigCookies\SetCookie::create($name); 92 | }, 93 | 'requestCookies' => new Dflydev\FigCookies\FigRequestCookies, 94 | 'responseCookies' => new Dflydev\FigCookies\FigResponseCookies, 95 | ]; 96 | }; 97 | 98 | // tokens 99 | $container['tokens'] = function ($c) { 100 | return new Godot\AssetLibrary\Helpers\Tokens($c); 101 | }; 102 | 103 | // utils 104 | $container['utils'] = function ($c) { 105 | return new Godot\AssetLibrary\Helpers\Utils($c); 106 | }; 107 | -------------------------------------------------------------------------------- /src/middleware.php: -------------------------------------------------------------------------------- 1 | getContainer(); 5 | 6 | $app->get('/', function ($request, $response) { 7 | return $response->withJson(['url' => 'asset']); 8 | }); 9 | 10 | $app->add(function ($request, $response, $next) { 11 | $cookie = $this->cookies['requestCookies']->get($request, 'token'); 12 | $body = $request->getParsedBody(); 13 | if ($cookie->getValue() !== null && !isset($body['token'])) { 14 | $cookieValue = (string) $cookie->getValue(); 15 | $body['token'] = $cookieValue; 16 | $request = $request->withParsedBody($body); 17 | } 18 | $response->getBody()->rewind(); 19 | $preresult = json_decode($response->getBody()->getContents(), true); 20 | if (!isset($preresult['error'])) { 21 | $response = $next($request, $response); 22 | } 23 | 24 | $static_routes = [ 25 | '/login' => true, 26 | '/register' => true, 27 | '/forgot_password' => true, 28 | '/change_password' => true, 29 | '/asset/submit' => true, 30 | ]; 31 | $queryUri = false; 32 | 33 | $route = $request->getAttribute('route'); 34 | $path = $request->getUri()->getPath(); 35 | 36 | if (isset($static_routes['/' . $path])) { 37 | $queryUri = '/' . $path; 38 | } elseif ($route) { 39 | $queryUri = $route->getPattern(); 40 | } else { 41 | return $response; 42 | } 43 | 44 | $queryUri = $request->getMethod() . ' ' . $queryUri; 45 | 46 | if ($route) { 47 | $response->getBody()->rewind(); 48 | $result = json_decode($response->getBody()->getContents(), true); 49 | if ($result === null) { 50 | return $response; 51 | //$result = ['error' => 'Can\'t decode api response - ' . $response->getBody()->getContents()]; 52 | } 53 | } else { 54 | $result = []; 55 | } 56 | 57 | if (isset($result['url'])) { 58 | $response = new \Slim\Http\Response(303); 59 | $response = $response->withHeader('Location', $request->getUri()->getBasePath() . '/' . $result['url']); 60 | } else { 61 | if (isset($result['token'])) { 62 | $body['token'] = $result['token']; 63 | } 64 | $template_names = [ 65 | 'GET /user/feed' => 'feed', 66 | 67 | 'GET /asset' => 'assets', 68 | 'GET /asset/submit' => 'submit_asset', 69 | 'GET /asset/{id:[0-9]+}' => 'asset', 70 | 'GET /asset/{id:[0-9]+}/edit' => 'edit_asset', 71 | 72 | 'GET /asset/edit' => 'asset_edits', 73 | 'GET /asset/edit/{id:[0-9]+}' => 'asset_edit', 74 | 'GET /asset/edit/{id:[0-9]+}/edit' => 'edit_asset_edit', 75 | 76 | 'GET /login' => 'login', 77 | 'ERROR POST /login' => 'login', 78 | 'GET /register' => 'register', 79 | 'ERROR POST /register' => 'register', 80 | 'GET /forgot_password' => 'forgot_password', 81 | 'POST /forgot_password' => 'forgot_password_result', 82 | 'GET /reset_password' => 'reset_password', 83 | 'GET /change_password' => 'change_password', 84 | 'ERROR POST /change_password' => 'change_password', 85 | 86 | 'ERROR' => 'error', 87 | ]; 88 | 89 | if (isset($result['error'])) { 90 | if (isset($template_names['ERROR ' . $queryUri])) { 91 | $queryUri = 'ERROR ' . $queryUri; 92 | } else { 93 | $queryUri = 'ERROR'; 94 | } 95 | } 96 | 97 | if (isset($template_names[$queryUri])) { 98 | $response = new \Slim\Http\Response(); 99 | $errorResponse = new \Slim\Http\Response(); 100 | $params = [ 101 | 'data' => $result, 102 | 'basepath' => $request->getUri()->getBasePath(). '', 103 | 'path' => $path, 104 | 'params' => $request->getQueryParams(), 105 | 'query' => $request->getUri()->getQuery(), 106 | 'categories' => [], // Filled later 107 | 'constants' => $this->constants, 108 | 'csrf_name_key' => $this->csrf->getTokenNameKey(), 109 | 'csrf_name' => $request->getAttribute('csrf_name'), 110 | 'csrf_value_key' => $this->csrf->getTokenValueKey(), 111 | 'csrf_value' => $request->getAttribute('csrf_value'), 112 | //'body' => $request->getParsedBody(), 113 | ]; 114 | 115 | if (isset($body['token'])) { 116 | $token = $this->tokens->validate($body['token']); 117 | $error = $this->utils->getUserFromTokenData(false, $errorResponse, $token, $user); 118 | if (!$error) { 119 | $params['user'] = $user; 120 | } else { 121 | $error = $this->utils->getUserFromTokenData(false, $errorResponse, $token, $reset_user, true); 122 | if (!$error) { 123 | $params['reset_user'] = $reset_user; 124 | } 125 | } 126 | } 127 | 128 | $query_categories = $this->queries['category']['list']; 129 | $query_categories->bindValue(':category_type', '%'); 130 | $query_categories->execute(); 131 | 132 | $error = $this->utils->errorResponseIfQueryBad(false, $errorResponse, $query_categories); 133 | $error = $this->utils->errorResponseIfQueryNoResults($error, $errorResponse, $query_categories); 134 | if (!$error) { 135 | $categories = $query_categories->fetchAll(); 136 | foreach ($categories as $key => $value) { 137 | $params['categories'][$value['id']] = $value; 138 | } 139 | } 140 | 141 | $response = $this->renderer->render($response, $template_names[$queryUri] . '.phtml', $params); 142 | $response = $response->withHeader('Content-Type', 'text/html'); 143 | } 144 | } 145 | 146 | if (isset($result['token'])) { 147 | $response = $this->cookies['responseCookies']->set($response, $this->cookies['setCookie']('token') 148 | ->withValue($result['token']) 149 | ->withDomain($request->getUri()->getHost()) 150 | ->withPath($request->getUri()->getBasePath()) 151 | ->withHttpOnly(true) 152 | ); 153 | } 154 | return $response; 155 | }); 156 | 157 | // Adding after the real middleware, since it has to run first... o.O 158 | $app->add($container->get('csrf')); 159 | 160 | $container->get('csrf')->setFailureCallable(function ($request, $response, $next) { 161 | $response = $response->withJson([ 162 | 'error' => 'CSRF check failed', 163 | ]); 164 | return $next($request, $response); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /src/queries.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'get_one' => 'SELECT user_id, username, email, password_hash, type FROM `as_users` WHERE user_id = :id', 6 | 'get_by_username' => 'SELECT user_id, username, email, password_hash, type FROM `as_users` WHERE username = :username', 7 | 'get_by_email' => 'SELECT user_id, username, email, password_hash, type FROM `as_users` WHERE email = :email', 8 | 'get_by_session_token' => 'SELECT user_id, username, email, password_hash, type FROM `as_users` WHERE session_token = :session_token', 9 | 'get_by_reset_token' => 'SELECT user_id, username, email, password_hash, type FROM `as_users` WHERE reset_token = :reset_token', 10 | 'set_session_token' => 'UPDATE `as_users` SET session_token = :session_token WHERE user_id = :id', 11 | 'set_reset_token' => 'UPDATE `as_users` SET reset_token = :reset_token WHERE user_id = :id', 12 | 'set_password_and_nullify_session' => 'UPDATE `as_users` SET password_hash = :password_hash, session_token = null WHERE user_id = :id', 13 | 'register' => 'INSERT INTO `as_users` SET username = :username, email = :email, password_hash = :password_hash', 14 | 'promote' => 'UPDATE `as_users` SET type = :type WHERE user_id = :id AND type < :type', 15 | // 'demote' => 'UPDATE `as_users` SET type = :type WHERE user_id = :id AND type > :type', 16 | 'list_edit_events' => 'SELECT edit_id, asset_id, COALESCE(`as_asset_edits`.title, `as_assets`.title) AS title, `as_asset_edits`.submit_date, `as_asset_edits`.modify_date, category, COALESCE(`as_asset_edits`.version_string, `as_assets`.version_string) AS version_string, COALESCE(`as_asset_edits`.icon_url, `as_assets`.icon_url) AS icon_url, status, reason FROM `as_asset_edits` 17 | LEFT JOIN `as_assets` USING (asset_id) 18 | LEFT JOIN `as_categories` ON `as_categories`.category_id = COALESCE(`as_asset_edits`.category_id, `as_assets`.category_id) 19 | WHERE `as_asset_edits`.user_id = :user_id 20 | ORDER BY `as_asset_edits`.modify_date DESC 21 | LIMIT :page_size OFFSET :skip_count', 22 | ], 23 | 'category' => [ 24 | 'list' => 'SELECT category_id as id, category as name, category_type as type FROM `as_categories` WHERE category_type LIKE :category_type ORDER BY category_id', 25 | ], 26 | 'asset' => [ 27 | 'search' => 'SELECT asset_id, title, username as author, user_id as author_id, category, category_id, godot_version, rating, cost, support_level, icon_url, version, version_string, modify_date FROM `as_assets` 28 | LEFT JOIN `as_users` USING (user_id) 29 | LEFT JOIN `as_categories` USING (category_id) 30 | 31 | WHERE searchable = TRUE AND category_id LIKE :category AND category_type LIKE :category_type 32 | AND support_level RLIKE :support_levels_regex AND username LIKE :username AND cost LIKE :cost 33 | AND godot_version <= :max_godot_version AND godot_version >= :min_godot_version 34 | AND ( 35 | title LIKE :filter 36 | OR cost LIKE :filter 37 | OR username LIKE :filter 38 | ) 39 | 40 | ORDER BY 41 | CASE 42 | WHEN :order_direction = "asc" THEN 43 | CASE 44 | WHEN :order = "rating" THEN rating 45 | WHEN :order = "cost" THEN cost 46 | WHEN :order = "title" THEN title 47 | WHEN :order = "modify_date" THEN modify_date 48 | END 49 | END ASC, 50 | CASE 51 | WHEN :order_direction = "desc" THEN 52 | CASE 53 | WHEN :order = "rating" THEN rating 54 | WHEN :order = "cost" THEN cost 55 | WHEN :order = "title" THEN title 56 | WHEN :order = "modify_date" THEN modify_date 57 | END 58 | END DESC 59 | 60 | LIMIT :page_size OFFSET :skip_count', 61 | 62 | 'search_count' => 'SELECT count(*) as count FROM `as_assets` 63 | LEFT JOIN `as_users` USING (user_id) 64 | LEFT JOIN `as_categories` USING (category_id) 65 | WHERE searchable = TRUE AND category_id LIKE :category AND category_type LIKE :category_type 66 | AND support_level RLIKE :support_levels_regex AND username LIKE :username AND cost LIKE :cost 67 | AND godot_version <= :max_godot_version AND godot_version >= :min_godot_version 68 | AND ( 69 | title LIKE :filter 70 | OR cost LIKE :filter 71 | OR username LIKE :filter 72 | )', 73 | 74 | 'get_one' => 'SELECT asset_id, category_type, title, username as author, user_id as author_id, version, version_string, category, category_id, godot_version, rating, cost, description, support_level, download_provider, download_commit, browse_url, issues_url, icon_url, preview_id, `as_asset_previews`.type, link, thumbnail, searchable, modify_date FROM `as_assets` 75 | LEFT JOIN `as_categories` USING (category_id) 76 | LEFT JOIN `as_users` USING (user_id) 77 | LEFT JOIN `as_asset_previews` USING (asset_id) 78 | WHERE asset_id = :id', 79 | 80 | 'get_one_bare' => 'SELECT * FROM `as_assets` WHERE asset_id = :asset_id', 81 | 'get_one_preview_bare' => 'SELECT * FROM `as_asset_previews` WHERE preview_id = :preview_id', 82 | 83 | 'apply_creational_edit' => 'INSERT INTO `as_assets` 84 | SET user_id=:user_id, title=:title, description=:description, category_id=:category_id, godot_version=:godot_version, 85 | version_string=:version_string, cost=:cost, 86 | download_provider=:download_provider, download_commit=:download_commit, browse_url=:browse_url, issues_url=:issues_url, icon_url=:icon_url, 87 | version=0+:update_version, support_level=:support_level, rating=0, searchable=TRUE', 88 | 89 | 'apply_edit' => 'UPDATE `as_assets` 90 | SET title=COALESCE(:title, title), description=COALESCE(:description, description), category_id=COALESCE(:category_id, category_id), godot_version=COALESCE(:godot_version, godot_version), version_string=COALESCE(:version_string, version_string), cost=COALESCE(:cost, cost), 91 | download_provider=COALESCE(:download_provider, download_provider), download_commit=COALESCE(:download_commit, download_commit), browse_url=COALESCE(:browse_url, browse_url), issues_url=COALESCE(:issues_url, issues_url), icon_url=COALESCE(:icon_url, icon_url), 92 | version=version+:update_version 93 | WHERE asset_id=:asset_id', 94 | 95 | 'apply_preview_edit_insert' => 'INSERT INTO `as_asset_previews` 96 | SET asset_id=:asset_id, type=:type, link=:link, thumbnail=:thumbnail', 97 | 'apply_preview_edit_remove' => 'DELETE FROM `as_asset_previews` 98 | WHERE preview_id=:preview_id AND asset_id=:asset_id', 99 | 'apply_preview_edit_update' => 'UPDATE `as_asset_previews` 100 | SET type=COALESCE(:type, type), link=COALESCE(:link, link), thumbnail=COALESCE(:thumbnail, thumbnail) 101 | WHERE preview_id=:preview_id AND asset_id=:asset_id', 102 | 103 | 'set_support_level' => 'UPDATE `as_assets` 104 | SET support_level=:support_level 105 | WHERE asset_id=:asset_id', 106 | 107 | 'delete' => 'UPDATE `as_assets` SET searchable=FALSE WHERE asset_id=:asset_id', 108 | 'undelete' => 'UPDATE `as_assets` SET searchable=TRUE WHERE asset_id=:asset_id' 109 | ], 110 | 'asset_edit' => [ 111 | 'get_one' => 'SELECT edit_id, `as_asset_edits`.asset_id, user_id, title, description, category_id, godot_version, version_string, 112 | cost, download_provider, download_commit, browse_url, issues_url, icon_url, status, reason, 113 | edit_preview_id, `as_asset_previews`.preview_id, `as_asset_edit_previews`.type, `as_asset_edit_previews`.link, `as_asset_edit_previews`.thumbnail, `as_asset_edit_previews`.operation, 114 | `as_asset_previews`.type AS orig_type, `as_asset_previews`.link AS orig_link, `as_asset_previews`.thumbnail AS orig_thumbnail, 115 | unedited_previews.preview_id AS unedited_preview_id, unedited_previews.type AS unedited_type, unedited_previews.link AS unedited_link, unedited_previews.thumbnail AS unedited_thumbnail, username AS author 116 | FROM `as_asset_edits` 117 | LEFT JOIN `as_users` USING (user_id) 118 | LEFT JOIN `as_asset_edit_previews` USING (edit_id) 119 | LEFT JOIN `as_asset_previews` USING (preview_id) 120 | LEFT JOIN `as_asset_previews` AS unedited_previews ON `as_asset_edits`.asset_id = unedited_previews.asset_id 121 | WHERE edit_id=:edit_id', 122 | 123 | 'get_one_bare' => 'SELECT * FROM `as_asset_edits` WHERE edit_id=:edit_id', 124 | 'get_one_with_status' => 'SELECT * FROM `as_asset_edits` WHERE edit_id=:edit_id AND status=:status', 125 | 'get_editable_by_asset_id' => 'SELECT * FROM `as_asset_edits` WHERE asset_id=:asset_id AND status=0', 126 | 127 | 'search' => 'SELECT edit_id, asset_id, 128 | `as_asset_edits`.user_id, 129 | `as_asset_edits`.submit_date, 130 | `as_asset_edits`.modify_date, 131 | COALESCE(`as_asset_edits`.title, `as_assets`.title) AS title, 132 | COALESCE(`as_asset_edits`.description, `as_assets`.description) AS description, 133 | COALESCE(`as_asset_edits`.godot_version, `as_assets`.godot_version) AS godot_version, 134 | COALESCE(`as_asset_edits`.version_string, `as_assets`.version_string) AS version_string, 135 | COALESCE(`as_asset_edits`.cost, `as_assets`.cost) AS cost, 136 | COALESCE(`as_asset_edits`.browse_url, `as_assets`.browse_url) AS browse_url, 137 | COALESCE(`as_asset_edits`.icon_url, `as_assets`.icon_url) AS icon_url, 138 | category, `as_assets`.support_level, status, reason, username AS author FROM `as_asset_edits` 139 | LEFT JOIN `as_users` USING (user_id) 140 | LEFT JOIN `as_categories` USING (category_id) 141 | LEFT JOIN `as_assets` USING (asset_id) 142 | WHERE 143 | status RLIKE :statuses_regex 144 | AND asset_id LIKE :asset_id AND username LIKE :username 145 | AND ( 146 | `as_asset_edits`.title LIKE :filter 147 | OR `as_assets`.title LIKE :filter 148 | OR username LIKE :filter 149 | ) 150 | ORDER BY `as_asset_edits`.modify_date DESC 151 | LIMIT :page_size OFFSET :skip_count', 152 | 153 | 'search_count' => 'SELECT count(*) AS count FROM `as_asset_edits` 154 | LEFT JOIN `as_users` USING (user_id) 155 | WHERE 156 | status RLIKE :statuses_regex 157 | AND asset_id LIKE :asset_id AND username LIKE :username 158 | AND ( 159 | title LIKE :filter 160 | OR username LIKE :filter 161 | ) 162 | ', 163 | 164 | 'submit' => 'INSERT INTO `as_asset_edits` 165 | SET asset_id=:asset_id, user_id=:user_id, title=:title, description=:description, category_id=:category_id, godot_version=:godot_version, version_string=:version_string, 166 | cost=:cost, download_provider=:download_provider, download_commit=:download_commit, browse_url=:browse_url, issues_url=:issues_url, icon_url=:icon_url, 167 | status=0, reason="", submit_date=NOW()', 168 | 169 | 'update' => 'UPDATE `as_asset_edits` 170 | SET title=:title, description=:description, category_id=:category_id, godot_version=:godot_version, version_string=:version_string, cost=:cost, 171 | download_provider=:download_provider, download_commit=:download_commit, browse_url=:browse_url, issues_url=:issues_url, icon_url=:icon_url 172 | WHERE edit_id=:edit_id AND status=0', 173 | 174 | 'add_preview' => 'INSERT INTO `as_asset_edit_previews` 175 | SET edit_id=:edit_id, preview_id=:preview_id, type=:type, link=:link, thumbnail=:thumbnail, operation=:operation', 176 | 'update_preview' => 'UPDATE `as_asset_edit_previews` 177 | SET type=COALESCE(:type, type), link=COALESCE(:link, link), thumbnail=COALESCE(:thumbnail, thumbnail) 178 | WHERE edit_id=:edit_id AND edit_preview_id=:edit_preview_id', 179 | 'remove_preview' => 'DELETE FROM `as_asset_edit_previews` 180 | WHERE edit_id=:edit_id AND edit_preview_id=:edit_preview_id', 181 | 182 | 'set_asset_id' => 'UPDATE `as_asset_edits` SET asset_id=:asset_id WHERE edit_id=:edit_id', 183 | 'set_status_and_reason' => 'UPDATE `as_asset_edits` SET status=:status, reason=:reason WHERE edit_id=:edit_id' 184 | ] 185 | ]; 186 | -------------------------------------------------------------------------------- /src/routes/asset.php: -------------------------------------------------------------------------------- 1 | get('/asset', function ($request, $response, $args) { 6 | $params = $request->getQueryParams(); 7 | 8 | $category = '%'; 9 | $filter = '%'; 10 | $username = '%'; 11 | $cost = '%'; 12 | $order_column = 'modify_date'; 13 | $support_levels = []; 14 | $page_size = 40; 15 | $max_page_size = 500; 16 | $page_offset = 0; 17 | $min_godot_version = 0; 18 | $max_godot_version = 9999999; 19 | if (FRONTEND) { 20 | $category_type = $this->constants['category_type']['any']; 21 | } else { 22 | $category_type = $this->constants['category_type']['addon']; 23 | $min_godot_version = 20100; 24 | $max_godot_version = 20199; 25 | } 26 | if (isset($params['category']) && $params['category'] != "") { 27 | $category = (int) $params['category']; 28 | } 29 | if (isset($params['type']) && isset($this->constants['category_type'][$params['type']])) { 30 | $category_type = $this->constants['category_type'][$params['type']]; 31 | } 32 | if (isset($params['support'])) { // Expects the param like `support=community+testing` or `support[community]=1&support[testing]=1&...` 33 | $support_levels = []; 34 | if (is_array($params['support'])) { 35 | foreach ($params['support'] as $key => $value) { 36 | if ($value && isset($this->constants['support_level'][$key])) { 37 | array_push($support_levels, (int) $this->constants['support_level'][$key]); 38 | } 39 | } 40 | } else { 41 | foreach (explode(' ', $params['support']) as $key => $value) { // `+` is changed to ` ` automatically 42 | if (isset($this->constants['support_level'][$value])) { 43 | array_push($support_levels, (int) $this->constants['support_level'][$value]); 44 | } 45 | } 46 | } 47 | } 48 | if (isset($params['filter'])) { 49 | $filter = '%'.preg_replace('/[[:punct:]]+/', '%', trim($params['filter'])).'%'; 50 | } 51 | if (isset($params['user'])) { 52 | $username = $params['user']; 53 | } 54 | if (isset($params['cost']) && $params['cost'] != "") { 55 | $cost = $params['cost']; 56 | } 57 | if (isset($params['max_results'])) { 58 | $page_size = min(abs((int) $params['max_results']), $max_page_size); 59 | } 60 | if (isset($params['godot_version']) && $params['godot_version'] != '') { 61 | if ($params['godot_version'] == 'any') { 62 | $min_godot_version = 0; 63 | $max_godot_version = 9999999; 64 | } else { 65 | $godot_version = $this->utils->getUnformattedGodotVersion($params['godot_version']); 66 | $min_godot_version = floor($godot_version / 10000) * 10000; // Keep just the major version 67 | $max_godot_version = floor($godot_version / 100) * 100 + 99; // The major was requested, give future patches 68 | // $max_godot_version = $godot_version; // Assume version requested can't handle future patches 69 | } 70 | } 71 | if (isset($params['page'])) { 72 | $page_offset = abs((int) $params['page']) * $page_size; 73 | } elseif (isset($params['offset'])) { 74 | $page_offset = abs((int) $params['offset']); 75 | } 76 | if (isset($params['sort'])) { 77 | $column_mapping = [ 78 | 'rating' => 'rating', 79 | 'cost' => 'cost', 80 | 'name' => 'title', 81 | 'updated' => 'modify_date' 82 | // TODO: downloads 83 | ]; 84 | if (isset($column_mapping[$params['sort']])) { 85 | $order_column = $column_mapping[$params['sort']]; 86 | } 87 | } 88 | 89 | // Sorting should be reversed by default only for dates, not strings 90 | $reverse = $order_column === 'modify_date'; 91 | if (isset($params['reverse'])) { 92 | $reverse = !$reverse; 93 | } 94 | 95 | if ($reverse) { 96 | $order_direction = 'desc'; 97 | } else { 98 | $order_direction = 'asc'; 99 | } 100 | 101 | if (count($support_levels) === 0) { 102 | $support_levels = [0, 1, 2]; // Testing + Community + Official / Featured 103 | } 104 | $support_levels = implode('|', $support_levels); 105 | 106 | $query = $this->queries['asset']['search']; 107 | $query->bindValue(':category', $category); 108 | $query->bindValue(':category_type', $category_type); 109 | $query->bindValue(':min_godot_version', $min_godot_version, PDO::PARAM_INT); 110 | $query->bindValue(':max_godot_version', $max_godot_version, PDO::PARAM_INT); 111 | $query->bindValue(':support_levels_regex', $support_levels); 112 | $query->bindValue(':filter', $filter); 113 | $query->bindValue(':username', $username); 114 | $query->bindValue(':cost', $cost); 115 | $query->bindValue(':order', $order_column); 116 | $query->bindValue(':order_direction', $order_direction); 117 | $query->bindValue(':page_size', $page_size, PDO::PARAM_INT); 118 | $query->bindValue(':skip_count', $page_offset, PDO::PARAM_INT); 119 | $query->execute(); 120 | 121 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 122 | if ($error) { 123 | return $response; 124 | } 125 | 126 | $query_count = $this->queries['asset']['search_count']; 127 | $query_count->bindValue(':category', $category); 128 | $query_count->bindValue(':category_type', $category_type); 129 | $query_count->bindValue(':min_godot_version', $min_godot_version, PDO::PARAM_INT); 130 | $query_count->bindValue(':max_godot_version', $max_godot_version, PDO::PARAM_INT); 131 | $query_count->bindValue(':support_levels_regex', $support_levels); 132 | $query_count->bindValue(':filter', $filter); 133 | $query_count->bindValue(':username', $username); 134 | $query_count->bindValue(':cost', $cost); 135 | $query_count->execute(); 136 | 137 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_count); 138 | if ($error) { 139 | return $response; 140 | } 141 | 142 | $total_count = $query_count->fetchAll()[0]['count']; 143 | 144 | $assets = $query->fetchAll(); 145 | 146 | $context = $this; 147 | $assets = array_map(function ($asset) use ($context) { 148 | $asset['godot_version'] = $this->utils->getFormattedGodotVersion((int) $asset['godot_version']); 149 | $asset['support_level'] = $context->constants['support_level'][(int) $asset['support_level']]; 150 | return $asset; 151 | }, $assets); 152 | 153 | return $response->withJson([ 154 | 'result' => $assets, 155 | 'page' => floor($page_offset / $page_size), 156 | 'pages' => ceil($total_count / $page_size), 157 | 'page_length' => $page_size, 158 | 'total_items' => (int) $total_count, 159 | ], 200); 160 | }); 161 | 162 | // Get information for a single asset 163 | $get_asset = function ($request, $response, $args) { 164 | $query = $this->queries['asset']['get_one']; 165 | 166 | $query->bindValue(':id', (int) $args['id'], PDO::PARAM_INT); 167 | $query->execute(); 168 | 169 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 170 | if ($error) { 171 | return $response; 172 | } 173 | 174 | if ($query->rowCount() <= 0) { 175 | return $response->withJson([ 176 | 'error' => 'Couldn\'t find asset with id '.$args['id'].'!' 177 | ], 404); 178 | } 179 | 180 | $output = $query->fetchAll(); 181 | $asset_info = []; 182 | $previews = []; 183 | 184 | foreach ($output as $row) { 185 | foreach ($row as $column => $value) { 186 | if ($value!==null) { 187 | if ($column==='preview_id') { 188 | $previews[] = ['preview_id' => $value]; 189 | } elseif ($column==="type" || $column==="link" || $column==="thumbnail") { 190 | $previews[count($previews) - 1][$column] = $value; 191 | } elseif ($column==="category_type") { 192 | $asset_info["type"] = $this->constants['category_type'][$value]; 193 | } elseif ($column==="support_level") { 194 | $asset_info["support_level"] = $this->constants['support_level'][(int) $value]; 195 | } elseif ($column==="download_provider") { 196 | $asset_info["download_provider"] = $this->constants['download_provider'][(int) $value]; 197 | } elseif ($column==="godot_version") { 198 | $asset_info["godot_version"] = $this->utils->getFormattedGodotVersion((int) $value); 199 | } else { 200 | $asset_info[$column] = $value; 201 | } 202 | } 203 | } 204 | } 205 | 206 | $asset_info['download_url'] = $this->utils->getComputedDownloadUrl($asset_info['browse_url'], $asset_info['download_provider'], $asset_info['download_commit']); 207 | if ($asset_info['issues_url'] == '') { 208 | $asset_info['issues_url'] = $this->utils->getDefaultIssuesUrl($asset_info['browse_url'], $asset_info['download_provider']); 209 | } 210 | 211 | 212 | foreach ($previews as $i => $_) { 213 | if (!isset($previews[$i]['thumbnail']) || $previews[$i]['thumbnail'] == '') { 214 | if ($previews[$i]['type'] == 'video') { 215 | $matches = []; 216 | if (preg_match('|youtube\.com/watch\?v=([^&]+)|', $previews[$i]['link'], $matches)) { 217 | $previews[$i]['thumbnail'] = 'https://img.youtube.com/vi/'.$matches[1].'/default.jpg'; 218 | } else { 219 | $previews[$i]['thumbnail'] = $previews[$i]['link']; 220 | } 221 | } else { 222 | $previews[$i]['thumbnail'] = $previews[$i]['link']; 223 | } 224 | } 225 | } 226 | 227 | $asset_info['previews'] = $previews; 228 | $asset_info['download_hash'] = ''; 229 | 230 | return $response->withJson($asset_info, 200); 231 | }; 232 | // Binding to multiple routes 233 | $app->get('/asset/{id:[0-9]+}', $get_asset); 234 | if (FRONTEND) { 235 | $app->get('/asset/{id:[0-9]+}/edit', $get_asset); 236 | } 237 | 238 | // Change support level of an asset 239 | $app->post('/asset/{id:[0-9]+}/support_level', function ($request, $response, $args) { 240 | $body = $request->getParsedBody(); 241 | 242 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user); 243 | $error = $this->utils->errorResponseIfNotUserHasLevel($error, $response, $user, 'moderator'); 244 | $error = $this->utils->errorResponseIfMissingOrNotString($error, $response, $body, 'support_level'); 245 | if ($error) { 246 | return $response; 247 | } 248 | if (!isset($this->constants['support_level'][$body['support_level']])) { 249 | $numeric_value_keys = []; 250 | foreach ($this->constants['support_level'] as $key => $value) { 251 | if ((int) $value === $value) { 252 | array_push($numeric_value_keys, $key); 253 | } 254 | } 255 | return $response->withJson([ 256 | 'error' => 'Invalid support level submitted, allowed are \'' . implode('\', \'', $numeric_value_keys) . '\'', 257 | ]); 258 | } 259 | 260 | $query = $this->queries['asset']['set_support_level']; 261 | 262 | $query->bindValue(':asset_id', (int) $args['id'], PDO::PARAM_INT); 263 | $query->bindValue(':support_level', (int) $this->constants['support_level'][$body['support_level']], PDO::PARAM_INT); 264 | 265 | $query->execute(); 266 | 267 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 268 | if ($error) { 269 | return $response; 270 | } 271 | 272 | return $response->withJson([ 273 | 'changed' => true, 274 | 'url' => 'asset/' . $args['id'], 275 | ], 200); 276 | }); 277 | 278 | /* 279 | * Delete asset from library 280 | */ 281 | $app->post('/asset/{id:[0-9]+}/delete', function ($request, $response, $args) { 282 | 283 | $body = $request->getParsedBody(); 284 | 285 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user); 286 | $error = $this->utils->errorResponseIfNotOwnerOrLevel($error, $response, $user, $args['id'], 'moderator'); 287 | 288 | if($error) return $response; 289 | 290 | $query = $this->queries['asset']['delete']; 291 | $query->bindValue(':asset_id', (int) $args['id'], PDO::PARAM_INT); 292 | $query->execute(); 293 | 294 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 295 | if($error) return $response; 296 | 297 | return $response->withJson([ 298 | 'changed' => true, 299 | 'url' => 'asset/' . $args['id'], 300 | ], 200); 301 | }); 302 | 303 | /* 304 | * Undelete asset from library 305 | */ 306 | $app->post('/asset/{id:[0-9]+}/undelete', function ($request, $response, $args) { 307 | 308 | $body = $request->getParsedBody(); 309 | 310 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user); 311 | $error = $this->utils->errorResponseIfNotOwnerOrLevel($error, $response, $user, $args['id'], 'moderator'); 312 | 313 | if($error) return $response; 314 | 315 | $query = $this->queries['asset']['undelete']; 316 | $query->bindValue(':asset_id', (int) $args['id'], PDO::PARAM_INT); 317 | $query->execute(); 318 | 319 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 320 | if($error) return $response; 321 | 322 | return $response->withJson([ 323 | 'changed' => true, 324 | 'url' => 'asset/' . $args['id'], 325 | ], 200); 326 | }); 327 | -------------------------------------------------------------------------------- /src/routes/auth.php: -------------------------------------------------------------------------------- 1 | get('/configure', function ($request, $response, $args) { 7 | $params = $request->getQueryParams(); 8 | 9 | $category_type = $this->constants['category_type']['addon']; 10 | 11 | if (isset($params['type']) && isset($this->constants['category_type'][$params['type']])) { 12 | $category_type = $this->constants['category_type'][$params['type']]; 13 | } 14 | 15 | $query = $this->queries['category']['list']; 16 | $query->bindValue(':category_type', $category_type); 17 | $query->execute(); 18 | 19 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 20 | if ($error) { 21 | return $response; 22 | } 23 | 24 | if (isset($request->getQueryParams()['session'])) { 25 | $id = openssl_random_pseudo_bytes($this->settings['auth']['tokenSessionBytesLength']); 26 | $token = $this->tokens->generate([ 27 | 'session' => base64_encode($id), 28 | ]); 29 | 30 | $uri = $request->getUri(); 31 | $uri = $uri->withPath((FRONTEND ? $uri->getBasePath() : dirname($uri->getBasePath())) . '/login') 32 | ->withQuery('') 33 | ->withFragment(urlencode($token)); 34 | 35 | return $response->withJson([ 36 | 'categories' => $query->fetchAll(), 37 | 'token' => $token, 38 | 'login_url' => (string) $uri, 39 | // ^ TODO: Make those routes actually work 40 | ], 200); 41 | } else { 42 | return $response->withJson([ 43 | 'categories' => $query->fetchAll(), 44 | ], 200); 45 | } 46 | }); 47 | 48 | $app->post('/register', function ($request, $response, $args) { 49 | $body = $request->getParsedBody(); 50 | $query = $this->queries['user']['register']; 51 | $query_check = $this->queries['user']['get_by_username']; 52 | 53 | $error = $this->utils->errorResponseIfMissingOrNotString(false, $response, $body, 'username'); 54 | $error = $this->utils->errorResponseIfMissingOrNotString($error, $response, $body, 'email'); 55 | $error = $this->utils->errorResponseIfMissingOrNotString($error, $response, $body, 'password'); 56 | if ($error) { 57 | return $response; 58 | } 59 | 60 | $query_check->bindValue(':username', $body['username']); 61 | $query_check->execute(); 62 | 63 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_check); 64 | if ($error) { 65 | return $response; 66 | } 67 | 68 | if ($query_check->rowCount() > 0) { 69 | return $response->withJson([ 70 | 'error' => 'Username already taken.', 71 | ], 409); 72 | } 73 | 74 | $password_hash = password_hash($body['password'], PASSWORD_BCRYPT, $this->get('settings')['auth']['bcryptOptions']); 75 | 76 | $query->bindValue(':username', $body['username']); 77 | $query->bindValue(':email', $body['email']); // TODO: Verify email. 78 | $query->bindValue(':password_hash', $password_hash); 79 | 80 | $query->execute(); 81 | 82 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 83 | if ($error) { 84 | return $response; 85 | } 86 | 87 | return $response->withJson([ 88 | 'username' => $body['username'], 89 | 'registered' => true, 90 | 'url' => 'login', 91 | ], 200); 92 | }); 93 | 94 | $app->post('/login', function ($request, $response, $args) { 95 | $body = $request->getParsedBody(); 96 | $query = $this->queries['user']['get_by_username']; 97 | 98 | $error = $this->utils->errorResponseIfMissingOrNotString(false, $response, $body, 'username'); 99 | $error = $this->utils->errorResponseIfMissingOrNotString($error, $response, $body, 'password'); 100 | if ($error) { 101 | return $response; 102 | } 103 | 104 | $query->bindValue(':username', $body['username']); 105 | $query->execute(); 106 | 107 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 108 | $error = $this->utils->errorResponseIfQueryNoResults(false, $response, $query, 'No such username: ' . $body['username']); 109 | if ($error) { 110 | return $response; 111 | } 112 | 113 | $user = $query->fetchAll()[0]; 114 | 115 | if (password_verify($body['password'], $user['password_hash'])) { 116 | if (isset($body['authorize_token'])) { 117 | $token_data = $this->tokens->validate($body['authorize_token']); 118 | 119 | if (!$token_data || !isset($token_data->session)) { 120 | return $response->withJson([ 121 | 'error' => 'Invalid token supplied' 122 | ], 400); 123 | } 124 | 125 | $session_id = $token_data->session; 126 | $token = $body['authorize_token']; 127 | } else { 128 | $session_id = openssl_random_pseudo_bytes($this->settings['auth']['tokenSessionBytesLength']); 129 | $token = $this->tokens->generate([ 130 | 'session' => base64_encode($session_id), 131 | ]); 132 | } 133 | 134 | $query_session = $this->queries['user']['set_session_token']; 135 | $query_session->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 136 | $query_session->bindValue(':session_token', $session_id); 137 | $query_session->execute(); 138 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_session); 139 | if ($error) { 140 | return $response; 141 | } 142 | 143 | return $response->withJson([ 144 | 'username' => $body['username'], 145 | 'token' => $token, 146 | 'authenticated' => true, 147 | 'url' => 'asset', 148 | ], 200); 149 | } else { 150 | return $response->withJson([ 151 | 'authenticated' => false, 152 | 'error' => 'Password doesn\'t match', 153 | ], 403); 154 | } 155 | }); 156 | 157 | $logout = function ($request, $response, $args) { 158 | $body = $request->getParsedBody(); 159 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user); 160 | 161 | $query = $this->queries['user']['set_session_token']; 162 | $query->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 163 | $query->bindValue(':session_token', null, PDO::PARAM_NULL); 164 | $query->execute(); 165 | 166 | return $response->withJson([ 167 | 'authenticated' => false, 168 | 'token' => '', 169 | 'url' => 'login', 170 | ], 200); 171 | }; 172 | 173 | if (FRONTEND) { 174 | $app->get('/logout', $logout); // Cookies would allow us to logout without post body. 175 | } else { 176 | $app->post('/logout', $logout); 177 | } 178 | 179 | $app->post('/forgot_password', function ($request, $response, $args) { 180 | $body = $request->getParsedBody(); 181 | 182 | $error = $this->utils->errorResponseIfMissingOrNotString(false, $response, $body, 'email'); 183 | if ($error) { 184 | return $response; 185 | } 186 | 187 | $query_user = $this->queries['user']['get_by_email']; 188 | $query_user->bindValue(':email', $body['email']); 189 | $query_user->execute(); 190 | 191 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_user); 192 | if ($error) { 193 | return $response; 194 | } 195 | 196 | 197 | if ($query_user->rowCount() != 0) { 198 | $user = $query_user->fetchAll()[0]; 199 | 200 | $reset_id = openssl_random_pseudo_bytes($this->settings['auth']['tokenResetBytesLength']); 201 | $token = $this->tokens->generate([ 202 | 'reset' => base64_encode($reset_id), 203 | ]); 204 | 205 | $query = $this->queries['user']['set_reset_token']; 206 | $query->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 207 | $query->bindValue(':reset_token', $reset_id); 208 | $query->execute(); 209 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 210 | if ($error) { 211 | return $response; 212 | } 213 | 214 | $reset_link = $request->getUri(); 215 | $base_path = (FRONTEND ? $reset_link->getBasePath() : dirname($reset_link->getBasePath())); 216 | $reset_link = $reset_link->withPath($base_path . '/reset_password') 217 | ->withQuery('token=' . urlencode($token)); 218 | 219 | $mail = $this->mail->__invoke(); // Since its a function closure, we have to invoke it with magic methods 220 | $mail->addAddress($user['email'], $user['username']); 221 | $mail->isHTML(true); 222 | $mail->Subject = "Password reset requested for $user[username]"; 223 | $mail->Body = $this->renderer->fetch('reset_password_email.phtml', [ 224 | 'user' => $user, 225 | 'link' => (string) $reset_link, 226 | ]); 227 | $mail->AltBody = "Reset your ($user[username]'s) password: $reset_link\n"; 228 | if (!$mail->send()) { 229 | $this->logger->error('mailSendFail', [$mail->ErrorInfo]); 230 | } 231 | // $this->logger->info('mailLinkDebug', [$reset_link]); 232 | } 233 | 234 | return $response->withJson([ 235 | 'email' => $body['email'], 236 | ], 200); 237 | }); 238 | 239 | $app->get('/reset_password', function ($request, $response, $args) { 240 | $params = $request->getQueryParams(); 241 | $body = null !== $request->getParsedBody()? $request->getParsedBody() : []; 242 | 243 | $error = $this->utils->ensureLoggedIn(false, $response, $params + $body, $user, $token_data, true); 244 | if ($error) { 245 | return $response; 246 | } 247 | 248 | $combined_body = $params + $body; 249 | 250 | return $response->withJson([ 251 | 'token' => $combined_body['token'], 252 | ], 200); 253 | }); 254 | 255 | $app->post('/reset_password', function ($request, $response, $args) { 256 | $body = $request->getParsedBody(); 257 | 258 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user, $token_data, true); 259 | $error = $this->utils->errorResponseIfMissingOrNotString(false, $response, $body, 'password'); 260 | if ($error) { 261 | return $response; 262 | } 263 | 264 | $password_hash = password_hash($body['password'], PASSWORD_BCRYPT, $this->get('settings')['auth']['bcryptOptions']); 265 | 266 | $query_password = $this->queries['user']['set_password_and_nullify_session']; 267 | $query_password->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 268 | $query_password->bindValue(':password_hash', $password_hash); 269 | $query_password->execute(); 270 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_password); 271 | if ($error) { 272 | return $response; 273 | } 274 | 275 | $query = $this->queries['user']['set_reset_token']; 276 | $query->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 277 | $query->bindValue(':reset_token', null, PDO::PARAM_NULL); 278 | $query->execute(); 279 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 280 | if ($error) { 281 | return $response; 282 | } 283 | 284 | return $response->withJson([ 285 | 'token' => null, 286 | 'url' => 'login', 287 | ], 200); 288 | }); 289 | 290 | $app->post('/change_password', function ($request, $response, $args) { 291 | $body = $request->getParsedBody(); 292 | 293 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user, $token_data); 294 | $error = $this->utils->errorResponseIfMissingOrNotString(false, $response, $body, 'new_password'); 295 | $error = $this->utils->errorResponseIfMissingOrNotString($error, $response, $body, 'old_password'); 296 | if ($error) { 297 | return $response; 298 | } 299 | 300 | if (!password_verify($body['old_password'], $user['password_hash'])) { 301 | return $response->withJson([ 302 | 'error' => 'Wrong old password supplied!', 303 | ], 403); 304 | } 305 | 306 | $password_hash = password_hash($body['new_password'], PASSWORD_BCRYPT, $this->get('settings')['auth']['bcryptOptions']); 307 | 308 | $query_password = $this->queries['user']['set_password_and_nullify_session']; 309 | $query_password->bindValue(':id', (int) $user['user_id'], PDO::PARAM_INT); 310 | $query_password->bindValue(':password_hash', $password_hash); 311 | $query_password->execute(); 312 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query_password); 313 | if ($error) { 314 | return $response; 315 | } 316 | 317 | return $response->withJson([ 318 | 'token' => null, 319 | 'url' => 'login', 320 | ], 200); 321 | }); 322 | -------------------------------------------------------------------------------- /src/routes/user.php: -------------------------------------------------------------------------------- 1 | getParsedBody(); 6 | 7 | $error = $this->utils->ensureLoggedIn(false, $response, $body, $user); 8 | if ($error) { 9 | return $response; 10 | } 11 | 12 | $page_size = 40; 13 | $max_page_size = 500; 14 | $page_offset = 0; 15 | if (isset($params['max_results'])) { 16 | $page_size = min(abs((int) $params['max_results']), $max_page_size); 17 | } 18 | if (isset($params['page'])) { 19 | $page_offset = abs((int) $params['page']) * $page_size; 20 | } elseif (isset($params['offset'])) { 21 | $page_offset = abs((int) $params['offset']); 22 | } 23 | 24 | $query = $this->queries['user']['list_edit_events']; 25 | $query->bindValue(':user_id', (int) $user['user_id'], PDO::PARAM_INT); 26 | $query->bindValue(':page_size', $page_size, PDO::PARAM_INT); 27 | $query->bindValue(':skip_count', $page_offset, PDO::PARAM_INT); 28 | $query->execute(); 29 | 30 | $error = $this->utils->errorResponseIfQueryBad(false, $response, $query); 31 | if ($error) { 32 | return $response; 33 | } 34 | 35 | $events = $query->fetchAll(); 36 | 37 | $context = $this; 38 | $events = array_map(function ($event) use ($context) { 39 | $event['status'] = $context->constants['edit_status'][(int) $event['status']]; 40 | return $event; 41 | }, $events); 42 | 43 | return $response->withJson([ 44 | 'events' => $events, 45 | ], 200); 46 | }; 47 | 48 | // Binding to multiple routes 49 | $app->post('/user/feed', $get_feed); 50 | if (FRONTEND) { 51 | $app->get('/user/feed', $get_feed); 52 | } 53 | -------------------------------------------------------------------------------- /src/settings-local-example.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'db' => [ 9 | 'dsn' => 'mysql:dbname=asset-library;host=127.0.0.1', 10 | 'user' => 'user', 11 | 'pass' => 'password', 12 | ], 13 | 'auth' => [ 14 | 'secret' => 'secret', 15 | ], 16 | 'mail' => [ 17 | 'from' => 'no-reply@localdomain.local', 18 | // 'replyTo' => '', 19 | // 'smtp' => [ 20 | // 'host' => '', 21 | // 'port' => 0, 22 | // 'auth' => ['user' => 'user', 'pass' => 'pass'], 23 | // 'secure' => '' 24 | // ], 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /src/settings.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'determineRouteBeforeAppMiddleware' => true, 5 | 'displayErrorDetails' => true, // set to false in production 6 | 7 | // Renderer settings 8 | 'renderer' => [ 9 | 'template_path' => __DIR__ . '/../templates/', 10 | ], 11 | 12 | // Monolog settings 13 | 'logger' => [ 14 | 'name' => 'slim-app', 15 | 'path' => __DIR__ . '/../logs/app.log', 16 | ], 17 | 18 | // PDO configuration 19 | 'db' => [ 20 | 'dsn' => 'mysql:dbname=asset-library;host=127.0.0.1', 21 | 'user' => 'user', // Check settings-local.php for those two 22 | 'pass' => 'pass', 23 | ], 24 | 25 | // Mail configuration 26 | 'mail' => [ 27 | 'from' => 'no-reply@localhost.localdomain', 28 | // 'replyTo' => '', 29 | // 'smtp' => [ 30 | // 'host' => '', 31 | // 'port' => 0, 32 | // 'auth' => ['user' => 'user', 'pass' => 'pass'], 33 | // 'secure' => '' 34 | // ], 35 | ], 36 | 37 | // Auth configuration 38 | 'auth' => [ 39 | 'secret' => 'somerandomstringshouldbeputhere', // Check settings-local.php 40 | 'tokenExpirationTime' => 3600 * 24 * 180, // 6 months 41 | 'tokenSessionBytesLength' => 24, // If set to something which isn't 24, change DB schema 42 | 'tokenResetBytesLength' => 32, // If set to something which isn't 32, change DB schema 43 | 'bcryptOptions' => [ 44 | 'cost' => 12, 45 | ], 46 | ], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /templates/.htaccess: -------------------------------------------------------------------------------- 1 | Order allow,deny 2 | Deny from all -------------------------------------------------------------------------------- /templates/_asset_fields.phtml: -------------------------------------------------------------------------------- 1 | '', 6 | 'category_id' => '0', 7 | 'godot_version' => '', 8 | 'version_string' => '', 9 | 'download_provider' => '', 10 | 'download_commit' => '', 11 | 'browse_url' => '', 12 | 'issues_url' => '', 13 | 'icon_url' => '', 14 | 'description' => '', 15 | 'cost' => '', 16 | 'previews' => [], 17 | ], $_asset_values); 18 | ?> 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 | 38 |
39 | 55 | 56 |
57 |
58 | 59 |
60 | 61 |
62 | 74 | The license under which the asset is available. Check opensource.org for a more detailed description of each. In case an OSI-approved license you are using is missing, please add the license in this file and open a pull request. 75 | Note: The license you specify here should also be included in the repository you are submitting under a standard name, such as LICENSE or LICENSE.md 76 |
77 |
78 | 79 |
80 | 81 |
82 | 97 | The site or service hosting your repository.
98 | The Custom download provider can be used to link to premade ZIP archives, such as those uploaded on GitHub Releases. This is useful for GDNative/GDExtension add-ons which contain precompiled binaries. This can also be useful when including submodules in the ZIP is desired (as submodules are not included in automatically generated ZIPs from GitHub or GitLab).
99 | If your repository host is missing, you might like to open an issue about it.
100 |
101 |
102 | 103 |
104 | 105 |
106 | 107 | The URL you use to browse your repository. 108 | Note: Do not give the clone URL (the one that ends in .git), but give the one you use to browse your code. 109 | When using the Custom download provider, this is used for browsing only. 110 |
111 |
112 | 113 |
114 | 115 |
116 | 117 | Optional, in case you are not using the one supplied by the repository host. Leave empty if unsure. 118 |
119 |
120 | 121 |
122 | 123 |
124 | 140 | 141 | The oldest version of Godot the asset works with.
142 | In the Godot editor and project manager, the asset will be displayed for all versions that are compatible (within the same major version).
143 | For example: 144 |
    145 |
  • If your asset works with Godot 4.1 and later, use 4.1 as the minimum Godot version.
  • 146 |
  • If your asset works with Godot 3.5 but not Godot 4.0 or later, use 3.5 as the minimum Godot version.
  • 147 |
148 |
149 |
150 |
151 | 152 |
153 | 154 |
155 | 156 | 157 |
158 |
159 | 160 |
161 | 162 |
163 | 164 | The commit hash that should be downloaded. Expects 40 or 64 hexadecimal digits fully specifying a Git commit. 165 | When using the Custom download provider, this must be set to the full download URL instead of a commit hash. 166 |
167 |
168 | 169 |
170 | 171 |
172 | 173 |
174 |
175 | 176 | 'image', 'link' => '', 'thumbnail' => '']; 179 | ?> 180 |
181 |
182 |
183 | 184 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 204 | 205 | 206 | 207 | 211 | 215 | 219 | 220 | 221 | 222 | 223 | 227 | 228 |
229 |
230 | 231 |
232 |
233 | 234 |
235 | 243 |
244 |
245 | 246 |
247 | 248 |
249 | 250 |
251 |
252 | 253 |
254 | 255 |
256 | 257 |
258 |
259 | 260 |
261 |
262 | 263 | -------------------------------------------------------------------------------- /templates/_csrf.phtml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/_footer.phtml: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/_header.phtml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <?php if(!empty($data['title'])){ echo(esc($data['title'].' - ')); } ?>Godot Asset Library 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /templates/_pagination.phtml: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 |
56 | Items per page: 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | item per page, item total. 66 |
67 |
68 | -------------------------------------------------------------------------------- /templates/asset.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | <?php echo esc($data['title']) ?>'s icon 5 |
6 |
7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | Deleted 15 | 16 | 17 | 18 | 19 | 25 |

26 | 27 |

28 | Submitted by user ; 29 | ; 30 | 31 |

32 |

33 | 34 |

35 |
36 |
37 |
38 |

39 | 40 | View files 41 | 42 | 43 | Download 44 | 45 | 46 | 47 | Submit an issue 48 | 49 | 50 | = $constants['user_type']['editor'])) { ?> 51 | 52 | Edit 53 | 54 | 55 | 56 | Recent Edits 57 | 58 |

59 |
60 |
61 | $preview) { ?> 62 | 74 | 75 |
76 | = $constants['user_type']['moderator'])) { ?> 77 |
78 |
79 | Admin tools 80 |
81 |
82 |
83 | 84 |
85 | 86 | 87 | 88 | 95 |
96 |
97 | 98 |
99 | 100 | 101 |
102 | 103 |
104 | 105 | 106 |
107 | 108 |
109 |
110 | 111 | 112 | -------------------------------------------------------------------------------- /templates/asset_edit.phtml: -------------------------------------------------------------------------------- 1 | 2 | 'Title', 4 | 'description' => 'Description', 5 | 'category_id' => 'Category', 6 | 'cost' => 'License', 7 | 'download_provider' => 'Repository Provider', 8 | 'browse_url' => 'Repository Url', 9 | 'issues_url' => 'Issues Url', 10 | 'godot_version' => 'Godot version', 11 | 'version_string' => 'Version String', 12 | 'download_commit' => 'Download Commit', 13 | 'download_url' => 'Download Url (Computed)', 14 | 'icon_url' => 'Icon Url', 15 | ]; 16 | $preview_field_names = [ 17 | 'type' => 'Type', 18 | 'link' => 'Image/Video URL', 19 | 'thumbnail' => 'Thumbnail', 20 | ]; ?> 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 | Godot 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |

46 | 47 | Edit of asset 48 | "" 49 | 50 | Creation of asset 51 | 52 | "" 53 | 54 | "" 55 | 56 | 57 | 58 | 59 | 60 |

61 | 62 | 63 |
64 | Edit modifications 65 |
66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | $name) { ?> 105 | 106 | 109 | 110 | 111 | 114 | 115 | 116 | 117 | 118 | 121 | 122 | 123 | 124 | 127 | 128 | 129 | 130 | 131 | 132 |
Old/CurrentNew/Edit
107 | 108 | 112 | 113 | 119 | 120 | 125 | 126 |
133 | $preview) if(isset($preview['edit_preview_id'])) { ?> 134 | 135 | 136 | 137 | 138 | 142 | 143 | 144 | 145 | $name) { ?> 146 | 147 | 150 | 151 | 152 | 155 | 156 | 157 | 158 | 159 | 160 | 163 | 164 | 167 | 170 | 171 | 172 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |
139 | Preview 140 | 141 |
148 | 149 | 153 | 154 | 161 | 162 | 165 | 166 | 168 | 169 | 174 | 175 |
183 | 184 | 185 | = $constants['user_type']['moderator']) { ?> 186 | 187 |
188 | 189 | 190 |
191 | 192 | 193 |
194 | 195 | 196 | 197 | 198 |
199 |
200 | 201 | 202 | 203 | 204 |
205 | 212 |
213 | 214 |
215 |
216 | 217 |
218 | 219 |
220 | 221 | 222 | 223 | 224 |
225 |
226 | 227 | 228 |
229 | 230 |
231 |
232 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /templates/asset_edits.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | $asset_edit) { ?> 65 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
StatusTypeAsset NameSubmit DateRevision Date
68 | 70 | 71 | 72 | format("Y-m-d")) ?>format("Y-m-d")) ?>
81 | 82 | 83 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /templates/assets.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | 93 | 94 | 95 | 100 | 101 |
102 | 103 |
104 |
    105 | $asset) { ?> 106 |
  1. 107 | 108 | <?php echo esc($asset['title']) ?>'s icon 109 |
    110 |

    111 |
    112 |
    113 | 114 | 115 | 121 |
    122 |
    123 | 124 |
    125 |
    126 |
    127 |
    128 | 132 |
  2. 133 | 134 |
135 | 136 | 137 |
138 |
139 | 140 | 141 | -------------------------------------------------------------------------------- /templates/change_password.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /templates/edit_asset.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 | Edit Asset 9 | 10 | Back 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | Cancel 22 | 23 |
24 |
25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /templates/edit_asset_edit.phtml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | Change edit 22 | 23 | Back 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | Cancel 35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /templates/error.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | Error: 4 |
5 | 6 | -------------------------------------------------------------------------------- /templates/feed.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | $event) { ?> 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
StatusTypeAsset NameSubmit DateRevision Date
16 | 18 | 19 | 20 |
29 | 30 | -------------------------------------------------------------------------------- /templates/forgot_password.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /templates/forgot_password_result.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /templates/index.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 13 |
14 |
15 | 18 |
19 |
20 | 21 | 22 | Edit Asset 23 | 24 | 25 |
26 | 27 |
28 | 29 | Set the name of your asset 30 |
31 |
32 | 33 | 34 |
35 | 36 |
37 | 43 |
44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | Set the version of your asset 52 |
53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 | Set the URL to download the asset (direct) 61 |
62 |
63 | 64 | 65 |
66 | 67 |
68 | 74 |
75 |
76 | 77 | 78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 | 86 |
87 | 88 |
89 | 90 |
91 |
92 | 93 | 94 |
95 | 96 |
97 | 98 |
99 |
100 | 101 | 102 |
103 | 104 |
105 | 106 |
107 |
108 | 109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 | 117 | 118 |
119 | 120 |
121 | 122 |
123 |
124 | 125 | 126 |
127 | 128 |
129 | 130 | Set the YouTube link demonstrating your asset 131 |
132 |
133 | 134 | 135 |
136 | 137 |
138 | 139 |
140 |
141 | 142 |
143 |
144 |
145 |
146 | 147 | -------------------------------------------------------------------------------- /templates/login.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | Register 31 | Forgot password 32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /templates/register.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | Login 27 | 28 |
29 |
30 | 31 | -------------------------------------------------------------------------------- /templates/reset_password.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /templates/reset_password_email.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reset password for <?php echo $user['username'] ?> on Godot asset library 6 | 7 | 8 | 9 |

Password reset requested for Godot asset library

10 |

11 | If you haven't requested the password reset you can disregard this email. 12 |

13 |

14 | If you aren't , you should disregard this email completely (or try to get in touch with the real , and forward it to him). 15 |

16 |

17 | Click this link to reset your password. 18 | (In case it isn't working, you can type/copy this url manually: ) 19 |

20 | 21 | 22 | -------------------------------------------------------------------------------- /templates/submit_asset.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | Submit Asset 8 |

9 | Before submitting, please read the 10 | 11 | Submitting to the Asset Library article for guidelines and instructions. 12 |

13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | --------------------------------------------------------------------------------