From f8eac67dac2f491ba52663d824db7bf0240f8515 Mon Sep 17 00:00:00 2001 From: pizzaboxer <41478239+pizzaboxer@users.noreply.github.com> Date: Sat, 31 Dec 2022 16:14:53 +0000 Subject: [PATCH] lmao --- .gitignore | 4 + RobloxOld.css | 11 + admin.php | 138 + ads.txt | 1 + api/account/asset/delete.php | 23 + api/account/character/get-assets.php | 72 + api/account/character/paint-body.php | 25 + api/account/character/request-render.php | 11 + api/account/character/toggle-wear.php | 65 + api/account/data/friends.php | 28 + api/account/data/groups.php | 137 + api/account/data/inventory.php | 45 + api/account/data/transactions.php | 36 + api/account/destroy-sessions.php | 20 + api/account/get-feed.php | 125 + api/account/get-recentlyplayed.php | 35 + api/account/get-transactions.php | 62 + api/account/update-password.php | 23 + api/account/update-ping.php | 8 + api/account/update-settings.php | 25 + api/account/update-status.php | 38 + api/admin/assetdelivery.php | 103 + api/admin/delete-news.php | 25 + api/admin/delete-post.php | 31 + api/admin/generate-password.php | 7 + api/admin/get-assets.php | 46 + api/admin/get-transactions.php | 64 + api/admin/getUnapprovedAssets.php | 47 + api/admin/git-pull.php | 39 + api/admin/giveCurrency.php | 30 + api/admin/moderateAsset.php | 41 + api/admin/moderateUser.php | 90 + api/admin/post-news.php | 27 + api/admin/previewModeration.php | 56 + api/admin/request-render.php | 68 + api/admin/upload.php | 119 + api/catalog/get-comments.php | 40 + api/catalog/post-comment.php | 34 + api/catalog/purchase.php | 82 + api/develop/getCreations.php | 40 + api/develop/upload.php | 168 + api/discord/check-verification.php | 28 + api/discord/whois.php | 34 + api/friends/accept-all.php | 16 + api/friends/accept.php | 26 + api/friends/get-friend-requests.php | 36 + api/friends/getFriendRequests.php | 40 + api/friends/getFriends.php | 51 + api/friends/revoke-all.php | 16 + api/friends/revoke.php | 26 + api/friends/send.php | 39 + api/games/fetch-running.php | 42 + api/games/fetch.php | 102 + api/games/placelauncher.php | 254 + api/games/shutdown.php | 26 + api/groups/admin/get-members.php | 55 + api/groups/admin/get-roles.php | 47 + api/groups/admin/request-relationship.php | 114 + api/groups/admin/update-member.php | 61 + api/groups/admin/update-relationship.php | 108 + api/groups/admin/update-roles.php | 170 + api/groups/delete-wall-post.php | 44 + api/groups/get-audit.php | 79 + api/groups/get-members.php | 53 + api/groups/get-related.php | 82 + api/groups/get-wall.php | 55 + api/groups/join-group.php | 35 + api/groups/leave-group.php | 26 + api/groups/post-shout.php | 59 + api/groups/post-wall.php | 41 + api/ide/toolbox.php | 121 + api/polygongs/gameserver.php | 361 + api/polygongs/report-gameserver-resources.php | 23 + api/polygongs/set-marker.php | 20 + api/polygongs/update-job.php | 72 + api/polygongs/verify-player.php | 33 + api/private/classes/Defuse/Crypto/Core.php | 457 + api/private/classes/Defuse/Crypto/Crypto.php | 445 + .../classes/Defuse/Crypto/DerivedKeys.php | 50 + .../classes/Defuse/Crypto/Encoding.php | 269 + .../Crypto/Exception/BadFormatException.php | 7 + .../Crypto/Exception/CryptoException.php | 7 + .../EnvironmentIsBrokenException.php | 7 + .../Defuse/Crypto/Exception/IOException.php | 7 + .../WrongKeyOrModifiedCiphertextException.php | 7 + api/private/classes/Defuse/Crypto/File.php | 778 + api/private/classes/Defuse/Crypto/Key.php | 94 + .../classes/Defuse/Crypto/KeyOrPassword.php | 149 + .../Defuse/Crypto/KeyProtectedByPassword.php | 145 + .../classes/Defuse/Crypto/RuntimeTests.php | 228 + .../classes/ParagonIE/ConstantTime/Base32.php | 471 + .../ParagonIE/ConstantTime/Base32Hex.php | 111 + .../classes/ParagonIE/ConstantTime/Base64.php | 271 + .../ParagonIE/ConstantTime/Base64DotSlash.php | 88 + .../ConstantTime/Base64DotSlashOrdered.php | 82 + .../ParagonIE/ConstantTime/Base64UrlSafe.php | 95 + .../classes/ParagonIE/ConstantTime/Binary.php | 85 + .../ConstantTime/EncoderInterface.php | 52 + .../ParagonIE/ConstantTime/Encoding.php | 260 + .../classes/ParagonIE/ConstantTime/Hex.php | 159 + .../ParagonIE/ConstantTime/RFC4648.php | 175 + .../ParagonIE/PasswordLock/PasswordLock.php | 221 + api/private/classes/Parsedown.php | 1775 ++ .../GoogleAuthenticator/FixedBitNotation.php | 292 + .../GoogleAuthenticator.php | 178 + .../GoogleAuthenticatorInterface.php | 44 + .../GoogleAuthenticator/GoogleQrUrl.php | 93 + .../GoogleAuthenticator/RuntimeException.php | 46 + api/private/classes/Verot/Upload/Upload.php | 5185 +++++ .../classes/pizzaboxer/ProjectPolygon/API.php | 128 + .../pizzaboxer/ProjectPolygon/Catalog.php | 193 + .../pizzaboxer/ProjectPolygon/Database.php | 77 + .../pizzaboxer/ProjectPolygon/Discord.php | 74 + .../ProjectPolygon/ErrorHandler.php | 128 + .../pizzaboxer/ProjectPolygon/Forum.php | 39 + .../pizzaboxer/ProjectPolygon/Games.php | 164 + .../pizzaboxer/ProjectPolygon/Groups.php | 164 + .../pizzaboxer/ProjectPolygon/Gzip.php | 58 + .../pizzaboxer/ProjectPolygon/Image.php | 73 + .../pizzaboxer/ProjectPolygon/PageBuilder.php | 193 + .../pizzaboxer/ProjectPolygon/Pagination.php | 57 + .../pizzaboxer/ProjectPolygon/Password.php | 49 + .../pizzaboxer/ProjectPolygon/Polygon.php | 230 + .../pizzaboxer/ProjectPolygon/RBXClient.php | 44 + .../pizzaboxer/ProjectPolygon/Session.php | 80 + .../pizzaboxer/ProjectPolygon/System.php | 32 + .../pizzaboxer/ProjectPolygon/Thumbnails.php | 150 + .../pizzaboxer/ProjectPolygon/Users.php | 289 + api/private/core.php | 499 + api/private/soap/Avatar.xml | 85 + api/private/soap/Clothing.xml | 84 + api/private/soap/Head.xml | 75 + api/private/soap/Mesh.xml | 79 + api/private/soap/Model.xml | 76 + api/private/soap/Place.xml | 86 + api/private/soap/UserModel.xml | 93 + api/private/templates/Body.php | 120 + api/private/templates/Body2014.php | 172 + api/private/templates/Error.php | 10 + api/private/templates/Footer.php | 149 + api/private/templates/Head.php | 32 + api/register/NameValidator.php | 22 + api/render/character.xml | 17 + api/render/characterasset.php | 2 + api/render/ping.php | 9 + api/render/update.php | 164 + api/users/get-badges.php | 72 + api/users/get-groups.php | 54 + api/users/get-inventory.php | 62 + browse.php | 132 + catalog.php | 249 + css/bootstrap-colorpicker.min.css | 10 + css/bootstrap-datepicker.min.css | 7 + css/bootstrap.min.css | 7 + css/bootstrap.min.css.map | 1 + css/fontawesome-all.min.css | 5 + css/fontawesome-pro-v5.10.1/css/all.css | 16592 ++++++++++++++++ css/fontawesome-pro-v5.10.1/css/all.min.css | 5 + css/fontawesome-pro-v5.10.1/css/fa-brands.css | 13 + .../css/fa-brands.min.css | 5 + .../css/fa-regular.css | 14 + .../css/fa-regular.min.css | 5 + css/fontawesome-pro-v5.10.1/css/fa-solid.css | 15 + .../css/fa-solid.min.css | 5 + .../css/fontawesome-all.css | 3203 +++ .../css/fontawesome-all.min.css | 5 + .../css/fontawesome.css | 3173 +++ .../css/fontawesome.min.css | 5 + .../less/_animated.less | 19 + .../less/_bordered-pulled.less | 16 + css/fontawesome-pro-v5.10.1/less/_core.less | 12 + .../less/_fixed-width.less | 6 + css/fontawesome-pro-v5.10.1/less/_icons.less | 992 + css/fontawesome-pro-v5.10.1/less/_larger.less | 27 + css/fontawesome-pro-v5.10.1/less/_list.less | 18 + css/fontawesome-pro-v5.10.1/less/_mixins.less | 57 + .../less/_rotated-flipped.less | 23 + .../less/_screen-reader.less | 5 + .../less/_stacked.less | 22 + .../less/_variables.less | 1001 + .../less/fa-brands.less | 21 + .../less/fa-regular.less | 22 + .../less/fa-solid.less | 23 + .../less/fontawesome.less | 16 + .../scss/_animated.scss | 20 + .../scss/_bordered-pulled.scss | 20 + css/fontawesome-pro-v5.10.1/scss/_core.scss | 16 + .../scss/_fixed-width.scss | 6 + css/fontawesome-pro-v5.10.1/scss/_icons.scss | 946 + css/fontawesome-pro-v5.10.1/scss/_larger.scss | 23 + css/fontawesome-pro-v5.10.1/scss/_list.scss | 18 + css/fontawesome-pro-v5.10.1/scss/_mixins.scss | 57 + .../scss/_rotated-flipped.scss | 23 + .../scss/_screen-reader.scss | 5 + .../scss/_stacked.scss | 31 + .../scss/_variables.scss | 959 + .../scss/fa-brands.scss | 21 + .../scss/fa-light.scss | 22 + .../scss/fa-regular.scss | 22 + .../scss/fa-solid.scss | 23 + .../scss/fontawesome.scss | 16 + .../webfonts/fa-brands-400.eot | Bin 0 -> 97180 bytes .../webfonts/fa-brands-400.svg | 990 + .../webfonts/fa-brands-400.ttf | Bin 0 -> 96944 bytes .../webfonts/fa-brands-400.woff | Bin 0 -> 62812 bytes .../webfonts/fa-brands-400.woff2 | Bin 0 -> 53800 bytes .../webfonts/fa-duotone-900-pro-5.0.0.woff2 | Bin 0 -> 40256 bytes .../webfonts/fa-duotone-900-pro-5.0.10.woff2 | Bin 0 -> 1244 bytes .../webfonts/fa-duotone-900-pro-5.0.11.woff2 | Bin 0 -> 4428 bytes .../webfonts/fa-duotone-900-pro-5.0.13.woff2 | Bin 0 -> 8940 bytes .../webfonts/fa-duotone-900-pro-5.0.3.woff2 | Bin 0 -> 1528 bytes .../webfonts/fa-duotone-900-pro-5.0.5.woff2 | Bin 0 -> 5164 bytes .../webfonts/fa-duotone-900-pro-5.0.7.woff2 | Bin 0 -> 6648 bytes .../webfonts/fa-duotone-900-pro-5.0.9.woff2 | Bin 0 -> 9844 bytes .../webfonts/fa-duotone-900-pro-5.1.0.woff2 | Bin 0 -> 16280 bytes .../webfonts/fa-duotone-900-pro-5.10.1.woff2 | Bin 0 -> 14328 bytes .../webfonts/fa-duotone-900-pro-5.2.0.woff2 | Bin 0 -> 16172 bytes .../webfonts/fa-duotone-900-pro-5.3.0.woff2 | Bin 0 -> 14952 bytes .../webfonts/fa-duotone-900-pro-5.4.0.woff2 | Bin 0 -> 16500 bytes .../webfonts/fa-duotone-900-pro-5.5.0.woff2 | Bin 0 -> 10248 bytes .../webfonts/fa-duotone-900-pro-5.6.0.woff2 | Bin 0 -> 13360 bytes .../webfonts/fa-duotone-900-pro-5.7.0.woff2 | Bin 0 -> 14752 bytes .../webfonts/fa-duotone-900-pro-5.8.0.woff2 | Bin 0 -> 2568 bytes .../webfonts/fa-duotone-900-pro-5.8.2.woff2 | Bin 0 -> 1500 bytes .../webfonts/fa-duotone-900-pro-5.9.0.woff2 | Bin 0 -> 13896 bytes .../webfonts/fa-light-300-pro-5.0.0.woff2 | Bin 0 -> 34916 bytes .../webfonts/fa-light-300-pro-5.0.10.woff2 | Bin 0 -> 1100 bytes .../webfonts/fa-light-300-pro-5.0.11.woff2 | Bin 0 -> 4384 bytes .../webfonts/fa-light-300-pro-5.0.13.woff2 | Bin 0 -> 8420 bytes .../webfonts/fa-light-300-pro-5.0.3.woff2 | Bin 0 -> 1436 bytes .../webfonts/fa-light-300-pro-5.0.5.woff2 | Bin 0 -> 4856 bytes .../webfonts/fa-light-300-pro-5.0.7.woff2 | Bin 0 -> 6372 bytes .../webfonts/fa-light-300-pro-5.0.9.woff2 | Bin 0 -> 9372 bytes .../webfonts/fa-light-300-pro-5.1.0.woff2 | Bin 0 -> 14832 bytes .../webfonts/fa-light-300-pro-5.10.1.woff2 | Bin 0 -> 13948 bytes .../webfonts/fa-light-300-pro-5.2.0.woff2 | Bin 0 -> 16276 bytes .../webfonts/fa-light-300-pro-5.3.0.woff2 | Bin 0 -> 14648 bytes .../webfonts/fa-light-300-pro-5.4.0.woff2 | Bin 0 -> 19240 bytes .../webfonts/fa-light-300-pro-5.5.0.woff2 | Bin 0 -> 11460 bytes .../webfonts/fa-light-300-pro-5.6.0.woff2 | Bin 0 -> 14260 bytes .../webfonts/fa-light-300-pro-5.7.0.woff2 | Bin 0 -> 15992 bytes .../webfonts/fa-light-300-pro-5.8.0.woff2 | Bin 0 -> 2388 bytes .../webfonts/fa-light-300-pro-5.8.2.woff2 | Bin 0 -> 1500 bytes .../webfonts/fa-light-300-pro-5.9.0.woff2 | Bin 0 -> 14704 bytes .../webfonts/fa-light-300.eot | Bin 0 -> 158288 bytes .../webfonts/fa-light-300.svg | 1875 ++ .../webfonts/fa-light-300.ttf | Bin 0 -> 158072 bytes .../webfonts/fa-light-300.woff | Bin 0 -> 70708 bytes .../webfonts/fa-light-300.woff2 | Bin 0 -> 55120 bytes .../webfonts/fa-regular-400-pro-5.0.0.woff2 | Bin 0 -> 27056 bytes .../webfonts/fa-regular-400-pro-5.0.10.woff2 | Bin 0 -> 1084 bytes .../webfonts/fa-regular-400-pro-5.0.11.woff2 | Bin 0 -> 3988 bytes .../webfonts/fa-regular-400-pro-5.0.13.woff2 | Bin 0 -> 8088 bytes .../webfonts/fa-regular-400-pro-5.0.5.woff2 | Bin 0 -> 4632 bytes .../webfonts/fa-regular-400-pro-5.0.7.woff2 | Bin 0 -> 5900 bytes .../webfonts/fa-regular-400-pro-5.0.9.woff2 | Bin 0 -> 8448 bytes .../webfonts/fa-regular-400-pro-5.1.0.woff2 | Bin 0 -> 11016 bytes .../webfonts/fa-regular-400-pro-5.10.1.woff2 | Bin 0 -> 11572 bytes .../webfonts/fa-regular-400-pro-5.2.0.woff2 | Bin 0 -> 14364 bytes .../webfonts/fa-regular-400-pro-5.3.0.woff2 | Bin 0 -> 13584 bytes .../webfonts/fa-regular-400-pro-5.4.0.woff2 | Bin 0 -> 17960 bytes .../webfonts/fa-regular-400-pro-5.5.0.woff2 | Bin 0 -> 10000 bytes .../webfonts/fa-regular-400-pro-5.6.0.woff2 | Bin 0 -> 12788 bytes .../webfonts/fa-regular-400-pro-5.7.0.woff2 | Bin 0 -> 14076 bytes .../webfonts/fa-regular-400-pro-5.8.0.woff2 | Bin 0 -> 2088 bytes .../webfonts/fa-regular-400-pro-5.8.2.woff2 | Bin 0 -> 1356 bytes .../webfonts/fa-regular-400-pro-5.9.0.woff2 | Bin 0 -> 13636 bytes .../webfonts/fa-regular-400.eot | Bin 0 -> 145480 bytes .../webfonts/fa-regular-400.svg | 1875 ++ .../webfonts/fa-regular-400.ttf | Bin 0 -> 145256 bytes .../webfonts/fa-regular-400.woff | Bin 0 -> 66320 bytes .../webfonts/fa-regular-400.woff2 | Bin 0 -> 52468 bytes .../webfonts/fa-solid-900-pro-5.0.0.woff2 | Bin 0 -> 8052 bytes .../webfonts/fa-solid-900-pro-5.0.5.woff2 | Bin 0 -> 2868 bytes .../webfonts/fa-solid-900-pro-5.0.7.woff2 | Bin 0 -> 2500 bytes .../webfonts/fa-solid-900-pro-5.0.9.woff2 | Bin 0 -> 4456 bytes .../webfonts/fa-solid-900-pro-5.10.1.woff2 | Bin 0 -> 5268 bytes .../webfonts/fa-solid-900-pro-5.3.0.woff2 | Bin 0 -> 6436 bytes .../webfonts/fa-solid-900-pro-5.4.0.woff2 | Bin 0 -> 9812 bytes .../webfonts/fa-solid-900-pro-5.5.0.woff2 | Bin 0 -> 6112 bytes .../webfonts/fa-solid-900-pro-5.6.0.woff2 | Bin 0 -> 5776 bytes .../webfonts/fa-solid-900-pro-5.7.0.woff2 | Bin 0 -> 8556 bytes .../webfonts/fa-solid-900-pro-5.8.0.woff2 | Bin 0 -> 940 bytes .../webfonts/fa-solid-900-pro-5.9.0.woff2 | Bin 0 -> 8308 bytes .../webfonts/fa-solid-900.eot | Bin 0 -> 127388 bytes .../webfonts/fa-solid-900.svg | 1875 ++ .../webfonts/fa-solid-900.ttf | Bin 0 -> 127172 bytes .../webfonts/fa-solid-900.woff | Bin 0 -> 56572 bytes .../webfonts/fa-solid-900.woff2 | Bin 0 -> 44420 bytes css/fontawesome-pro-v5.15.2/css/all.css | 5 + css/fontawesome-pro-v5.15.2/css/brands.css | 5 + .../css/fontawesome.css | 5 + css/fontawesome-pro-v5.15.2/css/light.css | 5 + css/fontawesome-pro-v5.15.2/css/regular.css | 5 + css/fontawesome-pro-v5.15.2/css/solid.css | 5 + css/fontawesome-pro-v5.15.2/scraper.py | 50 + .../webfonts/fa-brands-400.eot | Bin 0 -> 129590 bytes .../webfonts/fa-brands-400.svg | 3449 ++++ .../webfonts/fa-brands-400.ttf | Bin 0 -> 129284 bytes .../webfonts/fa-brands-400.woff | Bin 0 -> 87520 bytes .../webfonts/fa-brands-400.woff2 | Bin 0 -> 74652 bytes .../webfonts/fa-duotone-900.eot | Bin 0 -> 502130 bytes .../webfonts/fa-duotone-900.svg | 13243 ++++++++++++ .../webfonts/fa-duotone-900.ttf | Bin 0 -> 501832 bytes .../webfonts/fa-duotone-900.woff | Bin 0 -> 234468 bytes .../webfonts/fa-duotone-900.woff2 | Bin 0 -> 165712 bytes .../webfonts/fa-light-300.eot | Bin 0 -> 429282 bytes .../webfonts/fa-light-300.svg | 10593 ++++++++++ .../webfonts/fa-light-300.ttf | Bin 0 -> 429000 bytes .../webfonts/fa-light-300.woff | Bin 0 -> 218228 bytes .../webfonts/fa-light-300.woff2 | Bin 0 -> 164968 bytes .../webfonts/fa-regular-400.eot | Bin 0 -> 397226 bytes .../webfonts/fa-regular-400.svg | 9726 +++++++++ .../webfonts/fa-regular-400.ttf | Bin 0 -> 396932 bytes .../webfonts/fa-regular-400.woff | Bin 0 -> 201232 bytes .../webfonts/fa-regular-400.woff2 | Bin 0 -> 152180 bytes .../webfonts/fa-solid-900.eot | Bin 0 -> 338998 bytes .../webfonts/fa-solid-900.svg | 8280 ++++++++ .../webfonts/fa-solid-900.ttf | Bin 0 -> 338716 bytes .../webfonts/fa-solid-900.woff | Bin 0 -> 164092 bytes .../webfonts/fa-solid-900.woff2 | Bin 0 -> 123060 bytes css/fonts/ssprobold.woff | Bin 0 -> 28700 bytes css/fonts/ssprolight.woff | Bin 0 -> 26540 bytes css/fonts/ssproregular.woff | Bin 0 -> 29448 bytes css/fonts/ssprosemibold.woff | Bin 0 -> 29332 bytes css/polygon-2013.css | 89 + css/polygon-2014.css | 849 + css/polygon-dark.css | 178 + css/polygon-hitius.css | 26 + css/polygon-light.css | 10 + css/polygon.css | 430 + css/rbxclient/Tooolbox.css | 139 + css/simplemde.min.css | 7 + css/toastr.css | 6 + develop.php | 219 + directory_admin/create-asset.php | 630 + directory_admin/error-log.php | 50 + directory_admin/give-asset.php | 95 + directory_admin/give-currency.php | 63 + directory_admin/invites.php | 159 + directory_admin/manage-gameservers.php | 159 + directory_admin/moderate-assets.php | 47 + directory_admin/moderate-user.php | 226 + directory_admin/newsfeed.php | 119 + directory_admin/render-queue.php | 55 + directory_admin/site-banners.php | 136 + directory_admin/staff-audit.php | 113 + directory_admin/transactions.php | 81 + directory_forum/addpost.php | 174 + directory_forum/showpost.php | 135 + directory_login/2fa.php | 76 + directory_places/create.php | 375 + directory_places/update.php | 397 + discord.php | 1 + empty.php | 9 + error.php | 80 + farewell.php | 84 + favicon.ico | Bin 0 -> 1150 bytes forum.php | 191 + friends.php | 117 + game/fixassets.php | 47 + games.php | 109 + groups.php | 296 + home.php | 137 + img/2013/BuildPage/btn-gear_sprite_27px.png | Bin 0 -> 1419 bytes .../Buttons/Arrows/btn-silver-left-27.png | Bin 0 -> 1687 bytes .../Buttons/Arrows/btn-silver-right-27.png | Bin 0 -> 1720 bytes .../StyleGuide/bg-btn-blue-arrow-md.png | Bin 0 -> 1687 bytes img/2013/Buttons/StyleGuide/bg-btn-blue.png | Bin 0 -> 1944 bytes .../StyleGuide/bg-btn-gray-arrow-md.png | Bin 0 -> 1658 bytes img/2013/Buttons/StyleGuide/bg-btn-gray.png | Bin 0 -> 1552 bytes img/2013/Buttons/StyleGuide/bg-btn-green.png | Bin 0 -> 1901 bytes .../Buttons/StyleGuide/bg-lg-green-play.png | Bin 0 -> 8353 bytes img/2013/Buttons/bg-drop_down_btn.png | Bin 0 -> 1569 bytes img/2013/Buttons/bg-form_btn_lg-tile.png | Bin 0 -> 1245 bytes img/2013/Buttons/questionmark-12x12.png | Bin 0 -> 291 bytes img/2013/GamesPage/arrow_left.png | Bin 0 -> 1358 bytes img/2013/GamesPage/arrow_right.png | Bin 0 -> 1361 bytes .../Nav2014-icon-sprite-sheet.png | Bin 0 -> 13444 bytes .../StyleGuide/btn-control-large-tile.png | Bin 0 -> 994 bytes .../StyleGuide/btn-control-medium-tile.png | Bin 0 -> 983 bytes .../StyleGuide/btn-control-small-tile.png | Bin 0 -> 1036 bytes img/2013/roblox_logo.png | Bin 0 -> 7483 bytes img/PolygonChristmas.png | Bin 0 -> 128901 bytes img/PolygonStudio.png | Bin 0 -> 32251 bytes img/ProjectPolygon.ico | Bin 0 -> 1150 bytes img/ProjectPolygon.png | Bin 0 -> 68244 bytes img/TinyBcIcon.ico | Bin 0 -> 1150 bytes img/badges/1.png | Bin 0 -> 18561 bytes img/badges/2.png | Bin 0 -> 47953 bytes img/badges/CatalogManager.png | Bin 0 -> 64174 bytes img/badges/Friends.png | Bin 0 -> 85707 bytes img/badges/Moderator.png | Bin 0 -> 23864 bytes img/badges/Veteran.png | Bin 0 -> 93699 bytes img/error.png | Bin 0 -> 3347 bytes img/feed-starter.png | Bin 0 -> 5423 bytes img/feed/cart.png | Bin 0 -> 5130 bytes img/feed/friends.png | Bin 0 -> 5423 bytes img/korb.jpg | Bin 0 -> 24131 bytes img/landing/cr.png | Bin 0 -> 230411 bytes img/landing/gh.png | Bin 0 -> 1205155 bytes img/landing/polygonville-edit.png | Bin 0 -> 1320459 bytes img/landing/polygonville-edit2.jpg | Bin 0 -> 148677 bytes img/landing/polygonville-edit2.png | Bin 0 -> 1674331 bytes img/landing/polygonville-winter-edit.jpg | Bin 0 -> 454274 bytes img/landing/polygonville-winter-edit.png | Bin 0 -> 918300 bytes img/landing/polygonville-winter.png | Bin 0 -> 1072392 bytes img/landing/polygonville.png | Bin 0 -> 2027724 bytes img/landing/polygonwinter-edit.jpg | Bin 0 -> 641059 bytes img/landing/polygonwinter.jpg | Bin 0 -> 431499 bytes img/landing/skypanorama.png | Bin 0 -> 2250428 bytes img/spinners/ajax_loader_blue_300.gif | Bin 0 -> 24716 bytes img/spinners/loading_bluesquares.gif | Bin 0 -> 9639 bytes img/spinners/spinner100x100.gif | Bin 0 -> 8238 bytes img/spinners/spinner16x16.gif | Bin 0 -> 1737 bytes img/spinners/waiting-loader.gif | Bin 0 -> 3419 bytes img/spinners/waiting-spinner.gif | Bin 0 -> 3239 bytes img/tshirt-template.png | Bin 0 -> 28258 bytes index.html | 1 + index.php | 283 + info/privacy.php | 17 + info/terms-of-service.php | 22 + item.php | 204 + js/3D/MTLLoader.js | 472 + js/3D/OBJMTLLoader.js | 395 + js/3D/PolygonOrbitControls.js | 798 + js/3D/ThreeDeeThumbnails.js | 257 + js/3D/ThumbnailView.js | 177 + js/3D/three.min.js | 723 + js/3D/tween.js | 761 + js/Navigation2014.js | 265 + js/bootstrap-colorpicker.min.js | 10 + js/bootstrap-datepicker.min.js | 8 + js/bootstrap.bundle.min.js | 7 + js/bootstrap.bundle.min.js.map | 1 + js/custom-protocol-check-1.2.0.zip | Bin 0 -> 112139 bytes js/jquery-3.0.0.min.js | 4 + js/pako.min.js | 2 + js/polygon/Navigation2014.js | 210 + js/polygon/admin/asset-moderation.js | 92 + js/polygon/admin/transactions.js | 86 + js/polygon/catalog.js | 55 + js/polygon/character.js | 137 + js/polygon/core.js | 383 + js/polygon/friends.js | 147 + js/polygon/games.js | 330 + js/polygon/groups.js | 722 + js/polygon/home.js | 60 + js/polygon/inventory.js | 32 + js/polygon/item.js | 176 + js/polygon/money.js | 57 + js/polygon/profile.js | 74 + js/protocolcheck.js | 209 + js/protocolcheck.old.js | 2 + js/simplemde.min.js | 15 + js/snow.js | 112 + js/snowstorm.js | 665 + js/toastr.js | 7 + js/toastr.js.map | 1 + login.php | 111 + logout.php | 6 + moderation.php | 65 + my/account.php | 400 + my/character.php | 228 + my/creategroup.php | 274 + my/groupadmin.php | 363 + my/groupaudit.php | 90 + my/invites.php | 175 + my/item.php | 322 + my/money.php | 40 + my/stuff.php | 82 + nginx.conf | 402 + placeitem.php | 265 + rbxclient/analytics/gameperfmonitor.php | 215 + rbxclient/asset/bodycolors.php | 31 + rbxclient/asset/characterfetch.php | 79 + rbxclient/asset/fetch.php | 244 + rbxclient/asset/getscriptstate.html | 1 + rbxclient/error/dump.php | 5 + rbxclient/friend/arefriends.php | 17 + rbxclient/friend/breakfriend.php | 27 + rbxclient/friend/createfriend.php | 43 + rbxclient/game/clientpresence.php | 64 + rbxclient/game/edit.php | 74 + rbxclient/game/gameserver.php | 224 + rbxclient/game/getauthticket.php | 22 + rbxclient/game/help.php | 133 + rbxclient/game/join.php | 367 + rbxclient/game/keepalivepinger.php | 1 + rbxclient/game/knockouts.php | 15 + rbxclient/game/loadplaceinfo.php | 31 + .../luawebservice/handlesocialrequest.php | 57 + rbxclient/game/machineconfiguration.php | 4 + rbxclient/game/placevisit.php | 37 + rbxclient/game/studio.php | 40 + rbxclient/game/tools/insertasset.php | 3 + rbxclient/game/visit.php | 184 + rbxclient/game/wipeouts.php | 15 + rbxclient/login/negotiate.php | 65 + rbxclient/studio/publish-model.php | 267 + rbxclient/studio/publish-place.php | 290 + rbxclient/studio/toolbox.css | 0 rbxclient/studio/toolbox.php | 101 + rbxclient/studio/welcome.php | 104 + rbxclient/uploadmedia/screenshot.php | 20 + rbxclient/uploadmedia/video.php | 16 + robots.txt | 1 + thumbs/Script.png | Bin 0 -> 49482 bytes thumbs/asset.php | 79 + thumbs/asset3d.php | 17 + thumbs/audio.png | Bin 0 -> 55723 bytes thumbs/avatar.php | 15 + thumbs/avatar3d.php | 17 + thumbs/rawavatar.php | 18 + thumbs/resolvehash.php | 9 + user.php | 330 + 516 files changed, 121144 insertions(+) create mode 100644 .gitignore create mode 100644 RobloxOld.css create mode 100644 admin.php create mode 100644 ads.txt create mode 100644 api/account/asset/delete.php create mode 100644 api/account/character/get-assets.php create mode 100644 api/account/character/paint-body.php create mode 100644 api/account/character/request-render.php create mode 100644 api/account/character/toggle-wear.php create mode 100644 api/account/data/friends.php create mode 100644 api/account/data/groups.php create mode 100644 api/account/data/inventory.php create mode 100644 api/account/data/transactions.php create mode 100644 api/account/destroy-sessions.php create mode 100644 api/account/get-feed.php create mode 100644 api/account/get-recentlyplayed.php create mode 100644 api/account/get-transactions.php create mode 100644 api/account/update-password.php create mode 100644 api/account/update-ping.php create mode 100644 api/account/update-settings.php create mode 100644 api/account/update-status.php create mode 100644 api/admin/assetdelivery.php create mode 100644 api/admin/delete-news.php create mode 100644 api/admin/delete-post.php create mode 100644 api/admin/generate-password.php create mode 100644 api/admin/get-assets.php create mode 100644 api/admin/get-transactions.php create mode 100644 api/admin/getUnapprovedAssets.php create mode 100644 api/admin/git-pull.php create mode 100644 api/admin/giveCurrency.php create mode 100644 api/admin/moderateAsset.php create mode 100644 api/admin/moderateUser.php create mode 100644 api/admin/post-news.php create mode 100644 api/admin/previewModeration.php create mode 100644 api/admin/request-render.php create mode 100644 api/admin/upload.php create mode 100644 api/catalog/get-comments.php create mode 100644 api/catalog/post-comment.php create mode 100644 api/catalog/purchase.php create mode 100644 api/develop/getCreations.php create mode 100644 api/develop/upload.php create mode 100644 api/discord/check-verification.php create mode 100644 api/discord/whois.php create mode 100644 api/friends/accept-all.php create mode 100644 api/friends/accept.php create mode 100644 api/friends/get-friend-requests.php create mode 100644 api/friends/getFriendRequests.php create mode 100644 api/friends/getFriends.php create mode 100644 api/friends/revoke-all.php create mode 100644 api/friends/revoke.php create mode 100644 api/friends/send.php create mode 100644 api/games/fetch-running.php create mode 100644 api/games/fetch.php create mode 100644 api/games/placelauncher.php create mode 100644 api/games/shutdown.php create mode 100644 api/groups/admin/get-members.php create mode 100644 api/groups/admin/get-roles.php create mode 100644 api/groups/admin/request-relationship.php create mode 100644 api/groups/admin/update-member.php create mode 100644 api/groups/admin/update-relationship.php create mode 100644 api/groups/admin/update-roles.php create mode 100644 api/groups/delete-wall-post.php create mode 100644 api/groups/get-audit.php create mode 100644 api/groups/get-members.php create mode 100644 api/groups/get-related.php create mode 100644 api/groups/get-wall.php create mode 100644 api/groups/join-group.php create mode 100644 api/groups/leave-group.php create mode 100644 api/groups/post-shout.php create mode 100644 api/groups/post-wall.php create mode 100644 api/ide/toolbox.php create mode 100644 api/polygongs/gameserver.php create mode 100644 api/polygongs/report-gameserver-resources.php create mode 100644 api/polygongs/set-marker.php create mode 100644 api/polygongs/update-job.php create mode 100644 api/polygongs/verify-player.php create mode 100644 api/private/classes/Defuse/Crypto/Core.php create mode 100644 api/private/classes/Defuse/Crypto/Crypto.php create mode 100644 api/private/classes/Defuse/Crypto/DerivedKeys.php create mode 100644 api/private/classes/Defuse/Crypto/Encoding.php create mode 100644 api/private/classes/Defuse/Crypto/Exception/BadFormatException.php create mode 100644 api/private/classes/Defuse/Crypto/Exception/CryptoException.php create mode 100644 api/private/classes/Defuse/Crypto/Exception/EnvironmentIsBrokenException.php create mode 100644 api/private/classes/Defuse/Crypto/Exception/IOException.php create mode 100644 api/private/classes/Defuse/Crypto/Exception/WrongKeyOrModifiedCiphertextException.php create mode 100644 api/private/classes/Defuse/Crypto/File.php create mode 100644 api/private/classes/Defuse/Crypto/Key.php create mode 100644 api/private/classes/Defuse/Crypto/KeyOrPassword.php create mode 100644 api/private/classes/Defuse/Crypto/KeyProtectedByPassword.php create mode 100644 api/private/classes/Defuse/Crypto/RuntimeTests.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base32.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base32Hex.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base64.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base64DotSlash.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base64DotSlashOrdered.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Base64UrlSafe.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Binary.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/EncoderInterface.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Encoding.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/Hex.php create mode 100644 api/private/classes/ParagonIE/ConstantTime/RFC4648.php create mode 100644 api/private/classes/ParagonIE/PasswordLock/PasswordLock.php create mode 100644 api/private/classes/Parsedown.php create mode 100644 api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php create mode 100644 api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php create mode 100644 api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php create mode 100644 api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php create mode 100644 api/private/classes/Sonata/GoogleAuthenticator/RuntimeException.php create mode 100644 api/private/classes/Verot/Upload/Upload.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/API.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Catalog.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Database.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Discord.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/ErrorHandler.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Forum.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Games.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Groups.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Gzip.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Image.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/PageBuilder.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Pagination.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Password.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Polygon.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/RBXClient.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Session.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/System.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Thumbnails.php create mode 100644 api/private/classes/pizzaboxer/ProjectPolygon/Users.php create mode 100644 api/private/core.php create mode 100644 api/private/soap/Avatar.xml create mode 100644 api/private/soap/Clothing.xml create mode 100644 api/private/soap/Head.xml create mode 100644 api/private/soap/Mesh.xml create mode 100644 api/private/soap/Model.xml create mode 100644 api/private/soap/Place.xml create mode 100644 api/private/soap/UserModel.xml create mode 100644 api/private/templates/Body.php create mode 100644 api/private/templates/Body2014.php create mode 100644 api/private/templates/Error.php create mode 100644 api/private/templates/Footer.php create mode 100644 api/private/templates/Head.php create mode 100644 api/register/NameValidator.php create mode 100644 api/render/character.xml create mode 100644 api/render/characterasset.php create mode 100644 api/render/ping.php create mode 100644 api/render/update.php create mode 100644 api/users/get-badges.php create mode 100644 api/users/get-groups.php create mode 100644 api/users/get-inventory.php create mode 100644 browse.php create mode 100644 catalog.php create mode 100644 css/bootstrap-colorpicker.min.css create mode 100644 css/bootstrap-datepicker.min.css create mode 100644 css/bootstrap.min.css create mode 100644 css/bootstrap.min.css.map create mode 100644 css/fontawesome-all.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/all.css create mode 100644 css/fontawesome-pro-v5.10.1/css/all.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-brands.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-brands.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-regular.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-regular.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-solid.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fa-solid.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fontawesome-all.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fontawesome-all.min.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fontawesome.css create mode 100644 css/fontawesome-pro-v5.10.1/css/fontawesome.min.css create mode 100644 css/fontawesome-pro-v5.10.1/less/_animated.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_bordered-pulled.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_core.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_fixed-width.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_icons.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_larger.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_list.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_mixins.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_rotated-flipped.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_screen-reader.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_stacked.less create mode 100644 css/fontawesome-pro-v5.10.1/less/_variables.less create mode 100644 css/fontawesome-pro-v5.10.1/less/fa-brands.less create mode 100644 css/fontawesome-pro-v5.10.1/less/fa-regular.less create mode 100644 css/fontawesome-pro-v5.10.1/less/fa-solid.less create mode 100644 css/fontawesome-pro-v5.10.1/less/fontawesome.less create mode 100644 css/fontawesome-pro-v5.10.1/scss/_animated.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_bordered-pulled.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_core.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_fixed-width.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_icons.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_larger.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_list.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_mixins.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_rotated-flipped.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_screen-reader.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_stacked.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/_variables.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/fa-brands.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/fa-light.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/fa-regular.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/fa-solid.scss create mode 100644 css/fontawesome-pro-v5.10.1/scss/fontawesome.scss create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-brands-400.eot create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-brands-400.svg create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-brands-400.ttf create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-brands-400.woff create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-brands-400.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.10.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.11.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.13.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.3.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.5.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.7.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.0.9.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.1.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.10.1.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.2.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.3.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.4.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.5.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.6.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.7.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.8.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.8.2.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-duotone-900-pro-5.9.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.10.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.11.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.13.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.3.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.5.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.7.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.0.9.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.1.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.10.1.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.2.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.3.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.4.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.5.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.6.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.7.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.8.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.8.2.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300-pro-5.9.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300.eot create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300.svg create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300.ttf create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300.woff create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-light-300.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.10.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.11.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.13.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.5.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.7.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.0.9.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.1.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.10.1.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.2.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.3.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.4.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.5.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.6.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.7.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.8.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.8.2.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400-pro-5.9.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400.eot create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400.svg create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400.ttf create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400.woff create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-regular-400.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.0.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.0.5.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.0.7.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.0.9.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.10.1.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.3.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.4.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.5.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.6.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.7.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.8.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900-pro-5.9.0.woff2 create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900.eot create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900.svg create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900.ttf create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900.woff create mode 100644 css/fontawesome-pro-v5.10.1/webfonts/fa-solid-900.woff2 create mode 100644 css/fontawesome-pro-v5.15.2/css/all.css create mode 100644 css/fontawesome-pro-v5.15.2/css/brands.css create mode 100644 css/fontawesome-pro-v5.15.2/css/fontawesome.css create mode 100644 css/fontawesome-pro-v5.15.2/css/light.css create mode 100644 css/fontawesome-pro-v5.15.2/css/regular.css create mode 100644 css/fontawesome-pro-v5.15.2/css/solid.css create mode 100644 css/fontawesome-pro-v5.15.2/scraper.py create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-brands-400.eot create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-brands-400.svg create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-brands-400.ttf create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-brands-400.woff create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-brands-400.woff2 create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-duotone-900.eot create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-duotone-900.svg create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-duotone-900.ttf create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-duotone-900.woff create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-duotone-900.woff2 create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-light-300.eot create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-light-300.svg create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-light-300.ttf create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-light-300.woff create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-light-300.woff2 create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-regular-400.eot create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-regular-400.svg create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-regular-400.ttf create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-regular-400.woff create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-regular-400.woff2 create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-solid-900.eot create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-solid-900.svg create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-solid-900.ttf create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-solid-900.woff create mode 100644 css/fontawesome-pro-v5.15.2/webfonts/fa-solid-900.woff2 create mode 100644 css/fonts/ssprobold.woff create mode 100644 css/fonts/ssprolight.woff create mode 100644 css/fonts/ssproregular.woff create mode 100644 css/fonts/ssprosemibold.woff create mode 100644 css/polygon-2013.css create mode 100644 css/polygon-2014.css create mode 100644 css/polygon-dark.css create mode 100644 css/polygon-hitius.css create mode 100644 css/polygon-light.css create mode 100644 css/polygon.css create mode 100644 css/rbxclient/Tooolbox.css create mode 100644 css/simplemde.min.css create mode 100644 css/toastr.css create mode 100644 develop.php create mode 100644 directory_admin/create-asset.php create mode 100644 directory_admin/error-log.php create mode 100644 directory_admin/give-asset.php create mode 100644 directory_admin/give-currency.php create mode 100644 directory_admin/invites.php create mode 100644 directory_admin/manage-gameservers.php create mode 100644 directory_admin/moderate-assets.php create mode 100644 directory_admin/moderate-user.php create mode 100644 directory_admin/newsfeed.php create mode 100644 directory_admin/render-queue.php create mode 100644 directory_admin/site-banners.php create mode 100644 directory_admin/staff-audit.php create mode 100644 directory_admin/transactions.php create mode 100644 directory_forum/addpost.php create mode 100644 directory_forum/showpost.php create mode 100644 directory_login/2fa.php create mode 100644 directory_places/create.php create mode 100644 directory_places/update.php create mode 100644 discord.php create mode 100644 empty.php create mode 100644 error.php create mode 100644 farewell.php create mode 100644 favicon.ico create mode 100644 forum.php create mode 100644 friends.php create mode 100644 game/fixassets.php create mode 100644 games.php create mode 100644 groups.php create mode 100644 home.php create mode 100644 img/2013/BuildPage/btn-gear_sprite_27px.png create mode 100644 img/2013/Buttons/Arrows/btn-silver-left-27.png create mode 100644 img/2013/Buttons/Arrows/btn-silver-right-27.png create mode 100644 img/2013/Buttons/StyleGuide/bg-btn-blue-arrow-md.png create mode 100644 img/2013/Buttons/StyleGuide/bg-btn-blue.png create mode 100644 img/2013/Buttons/StyleGuide/bg-btn-gray-arrow-md.png create mode 100644 img/2013/Buttons/StyleGuide/bg-btn-gray.png create mode 100644 img/2013/Buttons/StyleGuide/bg-btn-green.png create mode 100644 img/2013/Buttons/StyleGuide/bg-lg-green-play.png create mode 100644 img/2013/Buttons/bg-drop_down_btn.png create mode 100644 img/2013/Buttons/bg-form_btn_lg-tile.png create mode 100644 img/2013/Buttons/questionmark-12x12.png create mode 100644 img/2013/GamesPage/arrow_left.png create mode 100644 img/2013/GamesPage/arrow_right.png create mode 100644 img/2013/Icons/Navigation2014/Nav2014-icon-sprite-sheet.png create mode 100644 img/2013/StyleGuide/btn-control-large-tile.png create mode 100644 img/2013/StyleGuide/btn-control-medium-tile.png create mode 100644 img/2013/StyleGuide/btn-control-small-tile.png create mode 100644 img/2013/roblox_logo.png create mode 100644 img/PolygonChristmas.png create mode 100644 img/PolygonStudio.png create mode 100644 img/ProjectPolygon.ico create mode 100644 img/ProjectPolygon.png create mode 100644 img/TinyBcIcon.ico create mode 100644 img/badges/1.png create mode 100644 img/badges/2.png create mode 100644 img/badges/CatalogManager.png create mode 100644 img/badges/Friends.png create mode 100644 img/badges/Moderator.png create mode 100644 img/badges/Veteran.png create mode 100644 img/error.png create mode 100644 img/feed-starter.png create mode 100644 img/feed/cart.png create mode 100644 img/feed/friends.png create mode 100644 img/korb.jpg create mode 100644 img/landing/cr.png create mode 100644 img/landing/gh.png create mode 100644 img/landing/polygonville-edit.png create mode 100644 img/landing/polygonville-edit2.jpg create mode 100644 img/landing/polygonville-edit2.png create mode 100644 img/landing/polygonville-winter-edit.jpg create mode 100644 img/landing/polygonville-winter-edit.png create mode 100644 img/landing/polygonville-winter.png create mode 100644 img/landing/polygonville.png create mode 100644 img/landing/polygonwinter-edit.jpg create mode 100644 img/landing/polygonwinter.jpg create mode 100644 img/landing/skypanorama.png create mode 100644 img/spinners/ajax_loader_blue_300.gif create mode 100644 img/spinners/loading_bluesquares.gif create mode 100644 img/spinners/spinner100x100.gif create mode 100644 img/spinners/spinner16x16.gif create mode 100644 img/spinners/waiting-loader.gif create mode 100644 img/spinners/waiting-spinner.gif create mode 100644 img/tshirt-template.png create mode 100644 index.html create mode 100644 index.php create mode 100644 info/privacy.php create mode 100644 info/terms-of-service.php create mode 100644 item.php create mode 100644 js/3D/MTLLoader.js create mode 100644 js/3D/OBJMTLLoader.js create mode 100644 js/3D/PolygonOrbitControls.js create mode 100644 js/3D/ThreeDeeThumbnails.js create mode 100644 js/3D/ThumbnailView.js create mode 100644 js/3D/three.min.js create mode 100644 js/3D/tween.js create mode 100644 js/Navigation2014.js create mode 100644 js/bootstrap-colorpicker.min.js create mode 100644 js/bootstrap-datepicker.min.js create mode 100644 js/bootstrap.bundle.min.js create mode 100644 js/bootstrap.bundle.min.js.map create mode 100644 js/custom-protocol-check-1.2.0.zip create mode 100644 js/jquery-3.0.0.min.js create mode 100644 js/pako.min.js create mode 100644 js/polygon/Navigation2014.js create mode 100644 js/polygon/admin/asset-moderation.js create mode 100644 js/polygon/admin/transactions.js create mode 100644 js/polygon/catalog.js create mode 100644 js/polygon/character.js create mode 100644 js/polygon/core.js create mode 100644 js/polygon/friends.js create mode 100644 js/polygon/games.js create mode 100644 js/polygon/groups.js create mode 100644 js/polygon/home.js create mode 100644 js/polygon/inventory.js create mode 100644 js/polygon/item.js create mode 100644 js/polygon/money.js create mode 100644 js/polygon/profile.js create mode 100644 js/protocolcheck.js create mode 100644 js/protocolcheck.old.js create mode 100644 js/simplemde.min.js create mode 100644 js/snow.js create mode 100644 js/snowstorm.js create mode 100644 js/toastr.js create mode 100644 js/toastr.js.map create mode 100644 login.php create mode 100644 logout.php create mode 100644 moderation.php create mode 100644 my/account.php create mode 100644 my/character.php create mode 100644 my/creategroup.php create mode 100644 my/groupadmin.php create mode 100644 my/groupaudit.php create mode 100644 my/invites.php create mode 100644 my/item.php create mode 100644 my/money.php create mode 100644 my/stuff.php create mode 100644 nginx.conf create mode 100644 placeitem.php create mode 100644 rbxclient/analytics/gameperfmonitor.php create mode 100644 rbxclient/asset/bodycolors.php create mode 100644 rbxclient/asset/characterfetch.php create mode 100644 rbxclient/asset/fetch.php create mode 100644 rbxclient/asset/getscriptstate.html create mode 100644 rbxclient/error/dump.php create mode 100644 rbxclient/friend/arefriends.php create mode 100644 rbxclient/friend/breakfriend.php create mode 100644 rbxclient/friend/createfriend.php create mode 100644 rbxclient/game/clientpresence.php create mode 100644 rbxclient/game/edit.php create mode 100644 rbxclient/game/gameserver.php create mode 100644 rbxclient/game/getauthticket.php create mode 100644 rbxclient/game/help.php create mode 100644 rbxclient/game/join.php create mode 100644 rbxclient/game/keepalivepinger.php create mode 100644 rbxclient/game/knockouts.php create mode 100644 rbxclient/game/loadplaceinfo.php create mode 100644 rbxclient/game/luawebservice/handlesocialrequest.php create mode 100644 rbxclient/game/machineconfiguration.php create mode 100644 rbxclient/game/placevisit.php create mode 100644 rbxclient/game/studio.php create mode 100644 rbxclient/game/tools/insertasset.php create mode 100644 rbxclient/game/visit.php create mode 100644 rbxclient/game/wipeouts.php create mode 100644 rbxclient/login/negotiate.php create mode 100644 rbxclient/studio/publish-model.php create mode 100644 rbxclient/studio/publish-place.php create mode 100644 rbxclient/studio/toolbox.css create mode 100644 rbxclient/studio/toolbox.php create mode 100644 rbxclient/studio/welcome.php create mode 100644 rbxclient/uploadmedia/screenshot.php create mode 100644 rbxclient/uploadmedia/video.php create mode 100644 robots.txt create mode 100644 thumbs/Script.png create mode 100644 thumbs/asset.php create mode 100644 thumbs/asset3d.php create mode 100644 thumbs/audio.png create mode 100644 thumbs/avatar.php create mode 100644 thumbs/avatar3d.php create mode 100644 thumbs/rawavatar.php create mode 100644 thumbs/resolvehash.php create mode 100644 user.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..843c6ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +thumbs/assets/* +thumbs/avatars/* +asset/files/* +api/private/config.php \ No newline at end of file diff --git a/RobloxOld.css b/RobloxOld.css new file mode 100644 index 0000000..50c2bf8 --- /dev/null +++ b/RobloxOld.css @@ -0,0 +1,11 @@ + +* +{ + font-size: 12px; + font-family: 'Comic Sans MS', Verdana, Arial, Helvetica, sans-serif; +} +H1 +{ + font-weight: bold; + font-size: larger; +} diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..aa4857b --- /dev/null +++ b/admin.php @@ -0,0 +1,138 @@ + "Moderator", + Users::STAFF_ADMINISTRATOR => "Administrator", + Users::STAFF_CATALOG => "Catalog Manager" +]; + +$servermemory = System::GetMemoryUsage(); +$usersOnline = Users::GetUsersOnline(); +$pendingRenders = Polygon::GetPendingRenders(); + +$usage = (object) +[ + "Memory" => (object) + [ + "Total" => System::GetFileSize($servermemory->total), + "SytemUsage" => System::GetFileSize($servermemory->total-$servermemory->free), + "PHPUsage" => System::GetFileSize(memory_get_usage(true)) + ], + + "Disk" => (object) + [ + "Total" => System::GetFileSize(disk_total_space("/")), + "SystemUsage" => System::GetFileSize(disk_total_space("/")-disk_free_space("/")), + "PolygonUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygon/"), + "PolygonSharedUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygonshared/"), + "ThumbnailUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygoncdn/"), + ] +]; + +$pageBuilder = new PageBuilder(["title" => SITE_CONFIG["site"]["name"]." Administration"]); +$pageBuilder->buildHeader(); +?> + + +
userId}\">{$row->username} - {$timestamp}
", + "message" => Polygon::FilterText($row->text) + ]; + } + else + { + $GroupInfo = Groups::GetGroupInfo($row->groupId, true, true); + $GroupInfo->name = htmlspecialchars($GroupInfo->name); + + $feed[] = + [ + "userName" => $GroupInfo->name, + "img" => Thumbnails::GetAssetFromID($GroupInfo->emblem), + "header" => "id}\">{$GroupInfo->name} - posted by userId}\">{$row->username} - {$timestamp}
", + "message" => Polygon::FilterText($row->text) + ]; + } +} + +$FeedCount = $FeedResults->rowCount(); + +if($FeedCount < 15) +{ + $feed[] = + [ + "userName" => "Your feed is currently empty!", + "img" => "/img/feed/friends.png", + "header" => "=$text["header"][$banType]?>
+Done at: =date('j/n/Y g:i:s A \G\M\T')?>
+Reason:
+', '
', $markdown->setEmbedsEnabled(true)->text($_POST["moderationNote"]))?> +
=$text["footer"][$banType]?>
+ +Reactivate + "POST", "admin" => Users::STAFF, "secure" => true]); + +$renderType = $_POST['renderType'] ?? false; +$assetID = $_POST['assetID'] ?? false; + +if(!$renderType) API::respond(400, false, "Bad Request"); +if(!in_array($renderType, ["Avatar", "Asset"])) API::respond(400, false, "Invalid render type"); +if(!$assetID || !is_numeric($assetID)) API::respond(400, false, "Bad Request"); + +if($renderType == "Asset") +{ + $asset = Catalog::GetAssetInfo($assetID); + if(!$asset) API::respond(200, false, "The asset you requested does not exist"); + switch($asset->type) + { + case 9: Polygon::RequestRender("Place", $assetID); break; //place + case 4: Polygon::RequestRender("Mesh", $assetID); break; // mesh + case 8: case 19: Polygon::RequestRender("Model", $assetID); break; // hat/gear + case 11: case 12: Polygon::RequestRender("Clothing", $assetID); break; // shirt/pants + case 17: Polygon::RequestRender("Head", $assetID); break; // head + case 10: Polygon::RequestRender("UserModel", $assetID); break; // user generated model + case 2: // t-shirt + $image = new Upload(SITE_CONFIG['paths']['assets'].$asset->imageID); + + Thumbnails::UploadAsset($image, $asset->imageID, 420, 420, ["keepRatio" => true, "align" => "T"]); + + //process initial tshirt thumbnail + $template = imagecreatefrompng($_SERVER['DOCUMENT_ROOT']."/img/tshirt-template.png"); + $shirtdecal = Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."{$asset->imageID}-420x420.png", 250, 250); + imagesavealpha($template, true); + imagesavealpha($shirtdecal, true); + Image::MergeLayers($template, $shirtdecal, 85, 85, 0, 0, 250, 250, 100); + + imagepng($template, SITE_CONFIG['paths']['thumbs_assets']."$assetID-420x420.png"); + + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."$assetID-420x420.png"); + break; + case 13: // decal + $image = new Upload(SITE_CONFIG['paths']['assets'].$asset->imageID); + + Thumbnails::UploadAsset($image, $asset->imageID, 420, 420, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $assetID, 420, 420); + break; + case 3: // audio + Image::RenderFromStaticImage("audio", $assetID); + break; + default: API::respond(200, false, "This asset cannot be re-rendered"); + } +} +else if($renderType == "Avatar") +{ + $user = Users::GetInfoFromID($assetID); + if(!$user) API::respond(200, false, "The user you requested does not exist"); + Polygon::RequestRender("Avatar", $assetID); +} + +Users::LogStaffAction("[ Render ] Re-rendered $renderType ID $assetID"); +API::respond(200, true, "Render request has been successfully submitted! See render status here"); \ No newline at end of file diff --git a/api/admin/upload.php b/api/admin/upload.php new file mode 100644 index 0000000..1470da2 --- /dev/null +++ b/api/admin/upload.php @@ -0,0 +1,119 @@ + "POST", "admin" => [Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "secure" => true]); + +$file = $_FILES["file"] ?? false; +$name = $_POST["name"] ?? ""; +$description = $_POST["description"] ?? ""; +$type = $_POST["type"] ?? false; +$uploadas = $_POST["creator"] ?? "Polygon"; +$creator = Users::GetIDFromName($uploadas); + +if(!$file) API::respond(200, false, "You must select a file"); +if(strlen($name) == 0) API::respond(200, false, "You must specify a name"); +if(strlen($name) > 50) API::respond(200, false, "Name cannot be longer than 50 characters"); +if(!$creator) API::respond(400, false, "The user you're trying to create as does not exist"); +if(Polygon::FilterText($name, false, false, true) != $name) API::respond(400, false, "The name contains inappropriate text"); + +//$lastCreation = $pdo->query("SELECT created FROM assets WHERE creator = 2 ORDER BY id DESC")->fetchColumn(); +//if($lastCreation+60 > time()) API::respond(400, false, "Please wait ".(60-(time()-$lastCreation))." seconds before creating a new asset"); + +if($type == 1) //image - this is for textures and stuff +{ + if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) API::respond(400, false, "Must be a png or jpg file"); + + $image = new Upload($file); + if(!$image->uploaded) API::respond(500, false, "Failed to process image - please contact an admin"); + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + + $imageId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "assets/"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); +} +elseif($type == 3) // audio +{ + if(!in_array($file["type"], ["audio/mpeg", "audio/ogg", "video/ogg", "audio/mid", "audio/wav"])) API::respond(400, false, "Must be an mpeg, wav, ogg or midi audio. - ".$file["type"]); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "audioType" => $file["type"], "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Image::RenderFromStaticImage("audio", $assetId); +} +elseif($type == 4) //mesh +{ + if(!str_ends_with($file["name"], ".mesh")) API::respond(400, false, "Must be a .mesh file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Polygon::RequestRender("Mesh", $assetId); +} +elseif($type == 5) //lua +{ + if(!str_ends_with($file["name"], ".lua")) API::respond(400, false, "Must be a .lua file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Image::RenderFromStaticImage("Script", $assetId); +} +elseif($type == 8) //hat +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Polygon::RequestRender("Model", $assetId); +} +elseif($type == 17) //head +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Polygon::RequestRender("Head", $assetId); +} +elseif($type == 18) //faces are literally just decals lmao (with a minor alteration to the xml) +{ + if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) API::respond(400, false, "Must be a png or jpg file"); + + $image = new Upload($file); + if(!$image->uploaded) API::respond(500, false, "Failed to process image - please contact an admin"); + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + + $imageId = Catalog::CreateAsset(["type" => 1, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "assets/"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "imageID" => $imageId, "approved" => 1]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Face", $imageId)); + + Thumbnails::UploadAsset($image, $itemId, 420, 420); +} +elseif($type == 19) //gear +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1, "gear_attributes" => '{"melee":false,"powerup":false,"ranged":false,"navigation":false,"explosive":false,"musical":false,"social":false,"transport":false,"building":false}']); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Polygon::RequestRender("Model", $assetId); +} +else if ($type == 24) // animation +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Image::RenderFromStaticImage("Animation", $assetId); +} + +Users::LogStaffAction("[ Asset creation ] Created \"$name\" [ID ".($itemId ?? $assetId ?? $imageId)."]"); +API::respondCustom([ + "status" => 200, + "success" => true, + "message" => "".Catalog::GetTypeByNum($type)." successfully created!", + "assetID" => ($itemId ?? $assetId ?? $imageId) +]); \ No newline at end of file diff --git a/api/catalog/get-comments.php b/api/catalog/get-comments.php new file mode 100644 index 0000000..dbb7796 --- /dev/null +++ b/api/catalog/get-comments.php @@ -0,0 +1,40 @@ +run("SELECT COUNT(*) FROM asset_comments WHERE assetID = :AssetID", [":AssetID" => $AssetID])->fetchColumn(); +if($CommentsCount == 0) API::respond(200, true, "This item does not have any comments"); + +$Pagination = Pagination($Page, $CommentsCount, 15); + +$Comments = Database::singleton()->run( + "SELECT asset_comments.*, users.username FROM asset_comments + INNER JOIN users ON users.id = asset_comments.author + WHERE assetID = :AssetID + ORDER BY id DESC LIMIT 15 OFFSET :Offset", + [":AssetID" => $AssetID, ":Offset" => $Pagination->Offset] +); + +$Items = []; + +while($Comment = $Comments->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "time" => strtolower(timeSince($Comment->time)), + "commenter_name" => $Comment->username, + "commenter_id" => $Comment->author, + "commenter_avatar" => Thumbnails::GetAvatar($Comment->author), + "content" => nl2br(Polygon::FilterText($Comment->content)) + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $Items, "pages" => $Pagination->Pages]); \ No newline at end of file diff --git a/api/catalog/post-comment.php b/api/catalog/post-comment.php new file mode 100644 index 0000000..d62e8cb --- /dev/null +++ b/api/catalog/post-comment.php @@ -0,0 +1,34 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST['assetID']) || !isset($_POST['content'])); + +$uid = SESSION["user"]["id"]; +$id = $_POST['assetID']; +$content = $_POST['content']; + +$item = Catalog::GetAssetInfo($id); +if(!$item) API::respond(400, false, "Asset does not exist"); +if(!$item->comments) API::respond(400, false, "Comments are unavailable for this asset"); +if(!strlen($content)) API::respond(400, false, "Comment cannot be empty"); +if(strlen($content) > 100) API::respond(400, false, "Comment cannot be longer than 128 characters"); + +$lastComment = Database::singleton()->run( + "SELECT time FROM asset_comments WHERE time+60 > UNIX_TIMESTAMP() AND author = :uid", + [":uid" => $uid] +); + +if($lastComment->rowCount()) API::respond(400, false, "Please wait ".GetReadableTime($lastComment->fetchColumn(), ["RelativeTime" => "1 minute"])." before posting a new comment"); + +Database::singleton()->run( + "INSERT INTO asset_comments (author, content, assetID, time) + VALUES (:uid, :content, :aid, UNIX_TIMESTAMP())", + [":uid" => $uid, ":content" => $content, ":aid" => $id] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/catalog/purchase.php b/api/catalog/purchase.php new file mode 100644 index 0000000..c8a9bd6 --- /dev/null +++ b/api/catalog/purchase.php @@ -0,0 +1,82 @@ + "POST", "logged_in" => true, "secure" => true]); + +function getPrice($price) +{ + return $price ? ' '.$price.'' : 'Free'; +} + +$uid = SESSION["user"]["id"]; +$id = $_POST['id'] ?? false; +$price = $_POST['price'] ?? 0; + +$item = Catalog::GetAssetInfo($id); +if(!$item) API::respond(400, false, "Asset does not exist"); +if(Catalog::OwnsAsset($uid, $id)) API::respond(400, false, "User already owns asset"); +if(!$item->sale) API::respond(400, false, "Asset is off-sale"); +if(SESSION["user"]["currency"] - $item->price < 0) API::respond(400, false, "User cannot afford asset"); + +if($item->price != $price) +{ + die(json_encode( + [ + "status" => 200, + "success" => true, + "message" => "Item price changed", + "header" => "Item Price Has Changed", + "text" => 'While you were shopping, the price of this item changed from '.getPrice($price).' to '.getPrice($item->price).'.', + "buttons" => [['class'=>'btn btn-success btn-confirm-purchase', 'text'=>'Buy Now'], ['class'=>'btn btn-secondary', 'dismiss'=>true, 'text'=>'Cancel']], + "footer" => 'Your balance after this transaction will be '.(SESSION["user"]["currency"] - $item->price), + "newprice" => $item->price + ])); +} + +$IsAlt = false; + +foreach(Users::GetAlternateAccounts($item->creator) as $alt) +{ + if($alt["userid"] == $uid) $IsAlt = true; +} + +if (!$IsAlt) +{ + Database::singleton()->run( + "UPDATE users SET currency = currency - :price WHERE id = :uid; + UPDATE users SET currency = currency + :price WHERE id = :seller", + [":price" => $item->price, ":uid" => $uid, ":seller" => $item->creator] + ); +} + +Database::singleton()->run( + "INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())", + [":aid" => $id, ":uid" => $uid] +); + +if ($item->creator != SESSION["user"]["id"]) +{ + Database::singleton()->run( + "INSERT INTO transactions (purchaser, seller, assetId, amount, flagged, timestamp) + VALUES (:uid, :sid, :aid, :price, :flagged, UNIX_TIMESTAMP())", + [":uid" => $uid, ":sid" => $item->creator, ":aid" => $id, ":price" => $item->price, ":flagged" => (int)$IsAlt] + ); + + Database::singleton()->run("UPDATE assets SET Sales = Sales + 1 WHERE id = :AssetID", [":AssetID" => $id]); +} + +die(json_encode( +[ + "status" => 200, + "success" => true, + "message" => "OK", + "header" => "Purchase Complete!", + "image" => Thumbnails::GetAsset($item), + "text" => "You have successfully purchased the ".htmlspecialchars($item->name)." ".Catalog::GetTypeByNum($item->type)." from ".$item->username." for ".getPrice($item->price), + "buttons" => [['class' => 'btn btn-primary continue-shopping', 'dismiss' => true, 'text' => 'Continue Shopping']], +])); diff --git a/api/develop/getCreations.php b/api/develop/getCreations.php new file mode 100644 index 0000000..94b39d8 --- /dev/null +++ b/api/develop/getCreations.php @@ -0,0 +1,40 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$type = $_POST["type"] ?? false; +$page = $_POST["page"] ?? 1; +$assets = []; + +if (!Catalog::GetTypeByNum($type)) API::respond(400, false, "Invalid asset type"); + +$assets = Database::singleton()->run( + "SELECT * FROM assets WHERE creator = :uid AND type = :type ORDER BY id DESC", + [":uid" => $userid, ":type" => $type] +); + +$items = []; + +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "name" => htmlspecialchars($asset->name), + "id" => $asset->id, + "version" => $asset->type == 9 ? $asset->Version : false, + "thumbnail" => Thumbnails::GetAsset($asset), + "item_url" => "/".encode_asset_name($asset->name)."-item?id={$asset->id}", + "config_url" => $asset->type == 9 ? "/places/{$asset->id}/update" : "/my/item?ID={$asset->id}", + "created" => date("j/n/Y", $asset->created), + "sales-total" => $asset->Sales, + "sales-week" => 0 //$info->sales_week + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "assets" => $items])); \ No newline at end of file diff --git a/api/develop/upload.php b/api/develop/upload.php new file mode 100644 index 0000000..eece5a7 --- /dev/null +++ b/api/develop/upload.php @@ -0,0 +1,168 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$file = $_FILES["file"] ?? false; +$name = $_POST["name"] ?? false; +$type = $_POST["type"] ?? false; + +if(!$file) API::respond(200, false, "You must select a file"); +if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"]) && $type != 10) API::respond(200, false, "Must be a .png or .jpg file"); + +if(empty($name)) API::respond(200, false, "You must specify a name"); +if(strlen($name) > 50) API::respond(200, false, "The name is too long"); +if(Polygon::IsExplicitlyFiltered($name)) API::respond(200, false, "The name contains inappropriate text"); + +if(!in_array($type, [2, 10, 11, 12, 13])) API::respond(200, false, "You can't upload that type of content!"); + +$lastCreation = Database::singleton()->run( + "SELECT created FROM assets WHERE creator = :uid ORDER BY id DESC", + [":uid" => $userid] +)->fetchColumn(); + +if($userid != 1 && $lastCreation+30 > time()) API::respond(200, false, "Please wait ".(30-(time()-$lastCreation))." seconds before creating a new asset"); + +// tshirts are a bit messy but straightforward: +// the image asset itself must be 128x128 with the texture resized to preserve aspect ratio +// the image thumbnail should have the texture positioned top +// +// shirts and pants should ideally be 585x559 but it doesnt really matter - +// just as long as it looks right on the avatar. if it doesnt then disapprove +// +// decals are a lot more messy: +// the image asset itself is scaled to be 256 pixels in width, while preserving the texture ratio +// the image thumbnail should have the texture positioned center +// the decal asset however must have the texture stretched to 1:1 for all its respective sizes +// [example: https://www.roblox.com/Item.aspx?ID=8553820] +// +// we won't have to worry about image size constraints as they're always gonna be +// resized to fit in a smaller resolution +// +// refer to here for the thumbnail sizes: https://github.com/matthewdean/roblox-web-apis +// +// THUMBNAIL SIZES FOR EACH ITEM TYPE +// legend: [f = fit] [t = top] [c = center] [s = stretch] // [M = Model] [He = Head] [S = Shirt] [P = Pants] +// +// | 48x48 | 60x62 | 75x75 | 100x100 | 110x110 | 160x100 | 250x250 | 352x352 | 420x230 | 420x420 | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// Image | |yes (f)| | | | | | | | yes (t) | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// T-Shirt | | | | yes | yes | | | | | yes | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// Audio | | | yes | yes | yes | | yes | yes | yes | yes | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// Hat/Gear | yes | | yes | yes | yes | | yes | yes | yes (fc)| yes | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// Place | yes |yes(fc)| yes | yes | yes | yes | yes | yes | yes | yes | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// M/He/S/P | yes | | yes | yes | yes | | yes | yes | | yes | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ +// Decal/Face |yes (s)| |yes (s)| yes (s) | yes (s) | | yes (s) | yes (s) | yes (s) | yes (s) | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ + +$image = new Upload($file); +if(!$image->uploaded) API::respond(200, false, "Failed to process image - please contact an admin"); +$image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; +$image->image_convert = 'png'; + +$imageId = Catalog::CreateAsset(["type" => 1, "creator" => SESSION["user"]["id"], "name" => $name, "description" => Catalog::GetTypeByNum($type)." Image"]); + +if ($type == 2) //tshirt +{ + $Processed = Image::Process($image, ["name" => "$imageId", "keepRatio" => true, "align" => "T", "x" => 128, "y" => 128, "dir" => "assets/"]); + if ($Processed !== true) API::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "T"]); + + $itemId = Catalog::CreateAsset(["type" => 2, "creator" => SESSION["user"]["id"], "name" => $name, "description" => "T-Shirt", "imageID" => $imageId]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("T-Shirt", $imageId)); + + //process initial tshirt thumbnail + $template = imagecreatefrompng($_SERVER['DOCUMENT_ROOT']."/img/tshirt-template.png"); + $shirtdecal = Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."/$imageId-420x420.png", 250, 250); + imagesavealpha($template, true); + imagesavealpha($shirtdecal, true); + Image::MergeLayers($template, $shirtdecal, 85, 85, 0, 0, 250, 250, 100); + + imagepng($template, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png"); + + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png"); +} +else if ($type == 11 || $type == 12) //shirt / pants +{ + $Processed = Image::Process($image, ["name" => "$imageId", "x" => 585, "y" => 559, "dir" => "assets/"]); + if ($Processed !== true) API::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => $type, "creator" => SESSION["user"]["id"], "name" => $name, "description" => Catalog::GetTypeByNum($type), "imageID" => $imageId]); + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML(Catalog::GetTypeByNum($type), $imageId)); + Polygon::RequestRender("Clothing", $itemId); +} +else if ($type == 10) // model +{ + $ModelXML = file_get_contents($file["tmp_name"]); + $ModelXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ROBLOXASSETURL%", $ModelXML); + $ModelXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ROBLOXASSETURL%", $ModelXML); + $isScript = stripos($ModelXML, 'class="Script" referent="RBX0"'); + + if (strlen($ModelXML) > 16000000) api::respond(200, false, "Model cannot be larger than 16 megabytes"); + + libxml_use_internal_errors(true); + $SimpleXML = simplexml_load_string($ModelXML); + + if ($SimpleXML === false) + { + api::respond(200, false, "Model File is invalid, are you sure it is an older format place file?"); + } + + $modelId = Catalog::CreateAsset([ + "type" => 10, + "creator" => SESSION["user"]["id"], + "name" => $name, + "description" => "Model", + "PublicDomain" => 0, + "approved" => $isScript ? 1 : 0 + ]); + + file_put_contents(Polygon::GetSharedResource("assets/{$modelId}"), $ModelXML); + Gzip::Compress(Polygon::GetSharedResource("assets/{$modelId}")); + + if ($isScript) + { + //put script image as thumbnail + Image::RenderFromStaticImage("Script", $modelId); + } + else + { + // user uploaded models are rendered as "usermodels" - this is just normal model rendering except there's no alpha + // no roblox thumbnails had transparency up until like 2013 anyway so its not that big of a deal + Polygon::RequestRender("UserModel", $modelId); + } +} +else if ($type == 13) //decal +{ + $Processed = Image::Process($image, ["name" => "$imageId", "x" => 256, "scaleY" => true, "dir" => "assets/"]); + if ($Processed !== true) api::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => 13, "creator" => SESSION["user"]["id"], "name" => $name, "description" => "Decal", "imageID" => $imageId]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Decal", $imageId)); + Thumbnails::UploadAsset($image, $itemId, 420, 420); +} + + +API::respond(200, true, Catalog::GetTypeByNum($type)." successfully created!"); \ No newline at end of file diff --git a/api/discord/check-verification.php b/api/discord/check-verification.php new file mode 100644 index 0000000..b0c30cf --- /dev/null +++ b/api/discord/check-verification.php @@ -0,0 +1,28 @@ + "GET", "api" => "DiscordBot"]); + +if (isset($_GET["Token"]) && isset($_GET["DiscordID"])) +{ + $userInfo = Database::singleton()->run("SELECT * FROM users WHERE discordKey = :key", [":key" => $_GET["Token"]])->fetch(\PDO::FETCH_OBJ); + if (!$userInfo) API::respond(200, false, "InvalidKey"); // check if verification key is valid + if ($userInfo->discordID != NULL) API::respond(200, false, "AlreadyVerified"); // check if mercury account is already verified + + Database::singleton()->run( + "UPDATE users SET discordID = :id, discordVerifiedTime = UNIX_TIMESTAMP() WHERE discordKey = :key", + [":id" => $_GET["DiscordID"], ":key" => $_GET["Token"]] + ); + + API::respond(200, true, $userInfo->username); +} +else if (isset($_GET["DiscordID"])) +{ + $username = Database::singleton()->run("SELECT username FROM users WHERE discordID = :id", [":id" => $_GET["DiscordID"]]); + if (!$username->rowCount()) API::respond(200, false, "NotVerified"); // check if discord account is already verified + API::respond(200, true, $username->fetchColumn()); +} + +API::respond(400, false, "Bad Request"); \ No newline at end of file diff --git a/api/discord/whois.php b/api/discord/whois.php new file mode 100644 index 0000000..a6c4585 --- /dev/null +++ b/api/discord/whois.php @@ -0,0 +1,34 @@ + "GET", "api" => "DiscordBot"]); + +if (isset($_GET["UserName"])) +{ + $userInfo = Database::singleton()->run( + "SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID, discordVerifiedTime FROM users WHERE username = :name", + [":name" => $_GET["UserName"]] + )->fetch(\PDO::FETCH_OBJ); + if (!$userInfo) API::respond(200, false, "DoesntExist"); +} +else if (isset($_GET["DiscordID"])) +{ + $userInfo = Database::singleton()->run( + "SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID, discordVerifiedTime FROM users WHERE discordID = :id", + [":id" => $_GET["DiscordID"]] + )->fetch(\PDO::FETCH_OBJ); + if (!$userInfo) API::respond(200, false, "NotVerified"); +} +else +{ + API::respond(400, false, "Bad Request"); +} + +$userInfo->thumbnail = Thumbnails::GetAvatar($userInfo->id, 420, 420, true); + +$userInfo->blurb = str_ireplace(["@everyone", "@here"], ["[everyone]", "[here]"], $userInfo->blurb); +$userInfo->blurb = preg_replace("/<(@[0-9]+)>/i", "[$1]", $userInfo->blurb); +API::respond(200, true, $userInfo); \ No newline at end of file diff --git a/api/friends/accept-all.php b/api/friends/accept-all.php new file mode 100644 index 0000000..19bdcac --- /dev/null +++ b/api/friends/accept-all.php @@ -0,0 +1,16 @@ + "POST", "logged_in" => true, "secure" => true]); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +if($RequestsCount == 0) API::respond(200, false, "You don't have any friend requests to accept right now"); + +Database::singleton()->run("UPDATE friends SET status = 1 WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]]); + +API::respond(200, true, "All your friend requests have been accepted"); \ No newline at end of file diff --git a/api/friends/accept.php b/api/friends/accept.php new file mode 100644 index 0000000..869dda4 --- /dev/null +++ b/api/friends/accept.php @@ -0,0 +1,26 @@ + "POST", "logged_in" => true, "secure" => true]); + +$FriendID = API::GetParameter("POST", "FriendID", "int"); + +$FriendRequest = Database::singleton()->run("SELECT * FROM friends WHERE id = :FriendID AND status = 0", [":FriendID" => $FriendID]); +$FriendRequestInfo = $FriendRequest->fetch(\PDO::FETCH_OBJ); + +if($FriendRequest->rowCount() == 0) API::respond(200, false, "Friend request doesn't exist"); +if((int) $FriendRequestInfo->receiverId != SESSION["user"]["id"]) API::respond(200, false, "You are not the recipient of this friend request"); + +Database::singleton()->run("UPDATE friends SET status = 1 WHERE id = :FriendID", [":FriendID" => $FriendID]); + +// since we're the one receiving it, we just need to update our pending requests +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id = :UserID", + [":UserID" => SESSION["user"]["id"]] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/friends/get-friend-requests.php b/api/friends/get-friend-requests.php new file mode 100644 index 0000000..ae4746b --- /dev/null +++ b/api/friends/get-friend-requests.php @@ -0,0 +1,36 @@ + "POST", "logged_in" => true, "secure" => true]); + +$Page = API::GetParameter("POST", "Page", "int", 1); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", + [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); +if($RequestsCount == 0) API::respond(200, true, "You're all up-to-date with your friend requests"); + +$Pagination = Pagination($Page, $RequestsCount, 18); + +$Requests = Database::singleton()->run( + "SELECT * FROM friends WHERE receiverId = :UserID AND status = 0 LIMIT 18 OFFSET :Offset", + [":UserID" => SESSION["user"]["id"], ":Offset" => $Pagination->Offset] +); + +while($Request = $Requests->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "Username" => Users::GetNameFromID($Request->requesterId), + "UserID" => $Request->requesterId, + "Avatar" => Thumbnails::GetAvatar($Request->requesterId), + "FriendID" => $Request->id + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $Items, "count" => (int) $RequestsCount, "pages" => (int) $Pagination->Pages]); \ No newline at end of file diff --git a/api/friends/getFriendRequests.php b/api/friends/getFriendRequests.php new file mode 100644 index 0000000..f76b5cc --- /dev/null +++ b/api/friends/getFriendRequests.php @@ -0,0 +1,40 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$page = $_POST['page'] ?? 1; + +$friendCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0", + [":uid" => $userid] +)->fetchColumn(); + +$pagination = Pagination($page, $friendCount, 18); + +if (!$pagination->Pages) API::respond(200, true, "You're all up-to-date with your friend requests!"); + +$friends = Database::singleton()->run( + "SELECT * FROM friends WHERE receiverId = :uid AND status = 0 LIMIT 18 OFFSET :offset", + [":uid" => $userid, ":offset" => $pagination->Offset] +); + +$items = []; + +while ($friend = $friends->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "username" => Users::GetNameFromID($friend->requesterId), + "userid" => $friend->requesterId, + "avatar" => Thumbnails::GetAvatar($friend->requesterId), + "friendid" => $friend->id + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "requests" => $items, "pages" => $pagination->Pages]); \ No newline at end of file diff --git a/api/friends/getFriends.php b/api/friends/getFriends.php new file mode 100644 index 0000000..3a2dffd --- /dev/null +++ b/api/friends/getFriends.php @@ -0,0 +1,51 @@ + "POST"]); + +$url = $_SERVER['HTTP_REFERER'] ?? false; +$userId = $_POST['userID'] ?? false; +$page = $_POST['page'] ?? 1; +$order = strpos($url, "/home") ? "lastonline DESC" : "id"; +$limit = strpos($url, "/friends") ? 18 : 6; +$self = str_ends_with($url, "/user") || str_ends_with($url, "/friends") || strpos($url, "/home"); + +if (!Users::GetInfoFromID($userId)) API::respond(400, false, "User does not exist"); + +$friendCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1", + [":uid" => $userId] +)->fetchColumn(); + +$pagination = Pagination($page, $friendCount, $limit); + +if (!$pagination->Pages) API::respond(200, true, ($self ? "You do" : Users::GetNameFromID($userId)." does")."n't have any friends"); + +$friends = Database::singleton()->run( + "SELECT friends.*, users.username, users.id AS userId, users.status, users.lastonline FROM friends + INNER JOIN users ON users.id = (CASE WHEN requesterId = :uid THEN receiverId ELSE requesterId END) + WHERE :uid IN (requesterId, receiverId) AND friends.status = 1 + ORDER BY {$order} LIMIT :limit OFFSET :offset", + [":uid" => $userId, ":limit" => $limit, ":offset" => $pagination->Offset] +); + +$items = []; + +while ($friend = $friends->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "username" => $friend->username, + "userid" => $friend->userId, + "avatar" => Thumbnails::GetAvatar($friend->userId), + "friendid" => $friend->id, + "status" => Polygon::FilterText($friend->status) + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $items, "pages" => $pagination->Pages]); \ No newline at end of file diff --git a/api/friends/revoke-all.php b/api/friends/revoke-all.php new file mode 100644 index 0000000..c44ae31 --- /dev/null +++ b/api/friends/revoke-all.php @@ -0,0 +1,16 @@ + "POST", "logged_in" => true, "secure" => true]); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +if($RequestsCount == 0) API::respond(200, false, "You don't have any friend requests to decline right now"); + +Database::singleton()->run("UPDATE friends SET status = 2 WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]]); + +API::respond(200, true, "All your friend requests have been decline"); \ No newline at end of file diff --git a/api/friends/revoke.php b/api/friends/revoke.php new file mode 100644 index 0000000..a522758 --- /dev/null +++ b/api/friends/revoke.php @@ -0,0 +1,26 @@ + "POST", "logged_in" => true, "secure" => true]); + +$FriendID = API::GetParameter("POST", "FriendID", "int"); + +$FriendConnection = Database::singleton()->run("SELECT * FROM friends WHERE id = :FriendID AND NOT status = 2", [":FriendID" => $FriendID]); +$FriendConnectionInfo = $FriendConnection->fetch(\PDO::FETCH_OBJ); + +if($FriendConnection->rowCount() == 0) API::respond(200, false, "Friend connection doesn't exist"); +if(!in_array(SESSION["user"]["id"], [$FriendConnectionInfo->requesterId, $FriendConnectionInfo->receiverId])) API::respond(200, false, "You are not a part of this friend connection"); + +Database::singleton()->run("UPDATE friends SET status = 2 WHERE id = :FriendID", [":FriendID" => $FriendID]); + +// a pending request revocation can come from either the sender or the receiver, so we update both +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id IN (:RequesterID, :ReceiverID)", + [":RequesterID" => $FriendConnectionInfo->requesterId, ":ReceiverID" => $FriendConnectionInfo->receiverId] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/friends/send.php b/api/friends/send.php new file mode 100644 index 0000000..283e155 --- /dev/null +++ b/api/friends/send.php @@ -0,0 +1,39 @@ + "POST", "logged_in" => true, "secure" => true]); + +$UserID = API::GetParameter("POST", "UserID", "int"); + +if ($UserID == SESSION["user"]["id"]) API::respond(200, false, "You can't perform friend operations on yourself"); +if (!Users::GetInfoFromID($UserID)) API::respond(200, false, "That user doesn't exist"); + +$FriendConnection = Database::singleton()->run( + "SELECT status FROM friends WHERE :UserID IN (requesterId, receiverId) AND :ReceiverID IN (requesterId, receiverId) AND NOT status = 2", + [":UserID" => SESSION["user"]["id"], ":ReceiverID" => $UserID] +); +if($FriendConnection->rowCount() != 0) API::respond(200, false, "Friend connection already exists"); + +$LastRequest = Database::singleton()->run( + "SELECT timeSent FROM friends WHERE requesterId = :UserID AND timeSent+60 > UNIX_TIMESTAMP()", + [":UserID" => SESSION["user"]["id"]] +); +if($LastRequest->rowCount() != 0) API::respond(200, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 minute"])." before sending another request"); + +Database::singleton()->run( + "INSERT INTO friends (requesterId, receiverId, timeSent) VALUES (:UserID, :ReceiverID, UNIX_TIMESTAMP())", + [":UserID" => SESSION["user"]["id"], ":ReceiverID" => $UserID] +); + +// update the user's pending requests +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id = :ReceiverID", + [":ReceiverID" => $UserID] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/games/fetch-running.php b/api/games/fetch-running.php new file mode 100644 index 0000000..6bf6a85 --- /dev/null +++ b/api/games/fetch-running.php @@ -0,0 +1,42 @@ + "POST", "logged_in" => true]); + +$PlaceID = API::GetParameter("POST", "PlaceID", "int", false); + +$GameJobCount = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobs WHERE Status = \"Ready\" AND PlaceID = :PlaceID", + [":PlaceID" => $PlaceID] +)->fetchColumn(); + +$Pagination = Pagination(API::GetParameter("POST", "Page", "int", 1), $GameJobCount, 5); + +$GameJobs = Database::singleton()->run( + "SELECT GameJobs.*, assets.MaxPlayers FROM GameJobs + INNER JOIN assets ON assets.id = PlaceID + WHERE Status = \"Ready\" AND PlaceID = :PlaceID + ORDER BY PlayerCount DESC LIMIT 5 OFFSET :Offset", + [":PlaceID" => $PlaceID, ":Offset" => $Pagination->Offset] +); + +if ($GameJobs->rowCount() == 0) +{ + API::respond(200, true, "No games are currently running for this place"); +} + +while ($GameJob = $GameJobs->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "JobID" => $GameJob->JobID, + "PlayerCount" => (int) $GameJob->PlayerCount, + "MaximumPlayers" => (int) $GameJob->MaxPlayers, + "IngamePlayers" => Games::GetPlayersInGame($GameJob->JobID) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "items" => $Items])); \ No newline at end of file diff --git a/api/games/fetch.php b/api/games/fetch.php new file mode 100644 index 0000000..43ddf9f --- /dev/null +++ b/api/games/fetch.php @@ -0,0 +1,102 @@ + "POST", "logged_in" => true]); + +$Filters = +[ + "Default" => "ServerRunning DESC, ActivePlayers DESC, LastServerUpdate DESC, updated DESC", + "Top Played" => "Visits DESC", + "Recently Updated" => "updated DESC" +]; + +$Query = API::GetParameter("POST", "Query", "string", ""); +$FilterBy = API::GetParameter("POST", "FilterBy", ["Default", "Top Played", "Recently Updated"], "Default"); +$Version = API::GetParameter("POST", "FilterVersion", ["All", "2010", "2011", "2012"], "All"); +$CreatorID = API::GetParameter("POST", "CreatorID", "int", false); + +$QueryParameters = "type = 9"; +$ValueParameters = []; + +if (strlen($Query)) +{ + $QueryParameters .= " AND name LIKE :Query"; + $ValueParameters[":Query"] = "%{$Query}%"; +} + +if ($Version != "All") +{ + $QueryParameters .= " AND Version = :Version"; + $ValueParameters[":Version"] = $Version; +} + +if ($CreatorID !== false) +{ + $Limit = 10; + $OrderBy = "created DESC"; + $QueryParameters .= " AND creator = :CreatorID"; + $ValueParameters[":CreatorID"] = $CreatorID; +} +else +{ + $Limit = 24; + $OrderBy = $Filters[$FilterBy]; + +} + +$PlaceCount = Database::singleton()->run("SELECT COUNT(*) FROM assets WHERE {$QueryParameters}", $ValueParameters)->fetchColumn(); + +$Pagination = Pagination(API::GetParameter("POST", "Page", "int", 1), $PlaceCount, $Limit); +$ValueParameters[":Limit"] = $Limit; +$ValueParameters[":Offset"] = $Pagination->Offset; + +$Places = Database::singleton()->run( + "SELECT assets.*, users.username FROM assets + INNER JOIN users ON users.id = assets.creator + WHERE {$QueryParameters} ORDER BY {$OrderBy} LIMIT :Limit OFFSET :Offset", + $ValueParameters +); + +if ($Places->rowCount() == 0) +{ + if ($CreatorID === false) + { + API::respond(200, true, "No games matched your query"); + } + else if ($CreatorID == SESSION["user"]["id"]) + { + API::respond(200, true, "You do not have any active places. Manage My Places"); + } + else + { + API::respond(200, true, Users::GetNameFromID($CreatorID) . " does not have any active places"); + } +} + +while ($Place = $Places->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "PlaceID" => (int) $Place->id, + "Name" => Polygon::FilterText($Place->name), + // "Description" => Polygon::FilterText($Place->description), + "Description" => Polygon::FilterText($markdown->setEmbedsEnabled(true)->line($Place->description), false), + "Visits" => number_format($Place->Visits), + "OnlinePlayers" => $Place->ServerRunning ? number_format($Place->ActivePlayers) : false, + "Location" => "/" . encode_asset_name($Place->name) . "-place?id={$Place->id}", + "Thumbnail" => Thumbnails::GetAsset($Place, 768, 432), + "CreatorName" => $Place->username, + "CreatorID" => $Place->creator, + "Version" => (int) $Place->Version, + "Uncopylocked" => (bool) $Place->publicDomain, + "CanPlayGame" => (bool) Games::CanPlayGame($Place) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "items" => $Items])); \ No newline at end of file diff --git a/api/games/placelauncher.php b/api/games/placelauncher.php new file mode 100644 index 0000000..f819b16 --- /dev/null +++ b/api/games/placelauncher.php @@ -0,0 +1,254 @@ + + [ + "Message" => "Waiting for a server", + "Code" => 0 + ], + + "Loading" => + [ + "Message" => "A server is loading the game", + "Code" => 1 + ], + + "Joining" => + [ + "Message" => "The server is ready. Joining the game...", + "Code" => 2 + ], + + "Error" => + [ + "Message" => "An error occured. Please try again later", + "Code" => 4 + ], + + "Expired" => + [ + "Message" => "There are no game servers available at this time. Please try again later.", + "Code" => 4 + ], + + "GameEnded" => + [ + "Message" => "The game you requested has ended", + "Code" => 5 + ], + + "GameFull" => + [ + "Message" => "The game you requested is full. Please try again later", + "Code" => 6 + ], + + "Ratelimit" => + [ + "Message" => "You are joining games too fast. Please try again later", + "Code" => 11 + ], + + "Unauthorized" => + [ + "Message" => "Cannot join game with no authenticated user.", + "Code" => 4 + ] +]; + +$IsTeleport = isset($_GET["isTeleport"]) && $_GET['isTeleport'] == "true"; + +if ($IsTeleport) +{ + $UserInfo = Users::GetInfoFromJobTicket(); +} +else +{ + API::initialize(["method" => "GET", "logged_in" => true, "secure" => true]); + $UserInfo = (object)SESSION["user"]; +} + +$Request = API::GetParameter("GET", "request", ["RequestGame", "RequestGameJob", "RequestFollowUser", "CheckGameJobStatus"]); + +if ($IsTeleport && GetUserAgent() != "Roblox/WinInet") +{ + die(json_encode([ + "Error" => "Request is not authorized from specified origin", + "userAgent" => $_SERVER["HTTP_USER_AGENT"] ?? null, + "referrer" => $_SERVER["HTTP_REFERER"] ?? null + ])); +} + +if (!$UserInfo) +{ + Respond("Unauthorized"); +} + +function Respond($Status, $JobID = null, $Version = null, $JoinScriptUrl = null) +{ + global $Statuses; + + $Response = []; + $StatusInfo = $Statuses[$Status]; + + $Response["jobId"] = $JobID; + $Response["status"] = $StatusInfo["Code"]; + $Response["joinScriptUrl"] = $JoinScriptUrl; + $Response["authenticationUrl"] = $JoinScriptUrl ? "https://{$_SERVER['HTTP_HOST']}/Login/Negotiate.ashx" : null; + $Response["authenticationTicket"] = $JoinScriptUrl ? "0" : null; + $Response["message"] = $StatusInfo["Message"]; + $Response["version"] = $Version; + + die(json_encode($Response)); +} + +function CheckRatelimit() +{ + global $UserInfo; + + $SessionsRequested = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobSessions WHERE UserID = :UserID AND TimeCreated + 60 > UNIX_TIMESTAMP()", + [":UserID" => $UserInfo->id] + )->fetchColumn(); + + if ($SessionsRequested >= 2) Respond("Ratelimit"); +} + +function CreateNewSession($Job) +{ + global $UserInfo, $IsTeleport; + + $Ticket = generateUUID(); + $SecurityTicket = generateUUID(); + + CheckRatelimit(); + + Database::singleton()->run( + "INSERT INTO GameJobSessions (Ticket, SecurityTicket, JobID, IsTeleport, UserID, TimeCreated) + VALUES (:Ticket, :SecurityTicket, :JobID, :IsTeleport, :UserID, UNIX_TIMESTAMP())", + [":Ticket" => $Ticket, ":SecurityTicket" => $SecurityTicket, ":JobID" => $Job->JobID, ":IsTeleport" => (int)$IsTeleport, ":UserID" => $UserInfo->id] + ); + + Respond("Joining", $Job->JobID, $Job->Version, "http://{$_SERVER['HTTP_HOST']}/Game/Join.ashx?JobTicket={$Ticket}"); +} + +if ($Request == "RequestGame") // clicking the "play" button +{ + $PlaceID = API::GetParameter("GET", "placeId", "int"); + $PlaceInfo = Database::singleton()->run("SELECT * FROM assets WHERE id = :PlaceID", [":PlaceID" => $PlaceID])->fetch(\PDO::FETCH_OBJ); + + if (!$PlaceInfo || $PlaceInfo->type != 9) Respond("Error"); + if ($IsTeleport && $PlaceInfo->Version != 2012) Respond("Error"); // TODO - add multi-client support for individual places? + + if (!Games::CanPlayGame($PlaceInfo)) + { + Respond("Error"); + } + + // check for an available game job + $AvailableJob = Database::singleton()->run( + "SELECT GameJobs.* FROM GameJobs + WHERE NOT Status IN (\"Closed\", \"Crashed\") + AND PlaceID = :PlaceID + AND PlayerCount < :MaxPlayers + LIMIT 1", + [":PlaceID" => $PlaceID, ":MaxPlayers" => $PlaceInfo->MaxPlayers] + )->fetch(\PDO::FETCH_OBJ); + + if ($AvailableJob) + { + if ($AvailableJob->Status == "Ready") + { + CreateNewSession($AvailableJob); + } + else + { + Respond($AvailableJob->Status == "Pending" ? "Waiting" : "Loading", $AvailableJob->JobID); + } + } + else + { + CheckRatelimit(); + + // get an available server + $GameServer = Database::singleton()->run( + "SELECT * FROM GameServers + WHERE Online + AND LastUpdated + 65 > UNIX_TIMESTAMP() + AND ActiveJobs < MaximumJobs + AND CpuUsage < 90 + AND AvailableMemory > 1024 + ORDER BY Priority ASC LIMIT 1" + )->fetch(\PDO::FETCH_OBJ); + + if (!$GameServer) Respond("Expired"); + + $JobID = generateUUID(); + + $ServersRequested = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobs WHERE RequestedBy = :UserID AND TimeCreated + 60 > UNIX_TIMESTAMP()", + [":UserID" => $UserInfo->id] + )->fetchColumn(); + + if ($ServersRequested >= 2) Respond("Error"); + + // request a new job + $GameJob = Database::singleton()->run( + "INSERT INTO GameJobs (RequestedBy, JobID, ServerID, Version, PlaceID, TimeCreated, LastUpdated) + VALUES (:UserID, :JobID, :ServerID, :Version, :PlaceID, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())", + [ + ":UserID" => $UserInfo->id, + ":JobID" => $JobID, + ":ServerID" => $GameServer->ServerID, + ":Version" => $PlaceInfo->Version, + ":PlaceID" => $PlaceInfo->id + ] + ); + + $Request = "{\"Operation\":\"OpenJob\", \"JobID\":\"{$JobID}\", \"Version\":{$PlaceInfo->Version}, \"PlaceID\":{$PlaceInfo->id}}"; + $Socket = fsockopen($GameServer->ServiceAddress, $GameServer->ServicePort); + fwrite($Socket, $Request); + fclose($Socket); + + Respond("Waiting", $JobID); + } +} +else if ($Request == "RequestFollowUser") // joining a user's game +{ + +} +else if ($Request == "RequestGameJob" || $Request == "CheckGameJobStatus") +{ + $JobID = API::GetParameter("GET", "jobId", "string"); + + // check for an available game job + $AvailableJob = Database::singleton()->run( + "SELECT GameJobs.*, assets.MaxPlayers FROM GameJobs + INNER JOIN assets ON assets.id = PlaceID + WHERE JobID = :JobID", + [":JobID" => $JobID] + )->fetch(\PDO::FETCH_OBJ); + + if (!Games::CanPlayGame((int)$AvailableJob->PlaceID)) + { + Respond("Error"); + } + + if (!$AvailableJob) Respond("Error"); + if ($AvailableJob->Status == "Closed" || $AvailableJob->Status == "Crashed") Respond("GameEnded"); + if ($AvailableJob->PlayerCount >= $AvailableJob->MaxPlayers) Respond("GameFull"); + + if ($AvailableJob->Status == "Pending") Respond("Waiting", $JobID); + if ($AvailableJob->Status == "Loading") Respond("Loading", $JobID); + + CreateNewSession($AvailableJob); +} \ No newline at end of file diff --git a/api/games/shutdown.php b/api/games/shutdown.php new file mode 100644 index 0000000..1e12ca5 --- /dev/null +++ b/api/games/shutdown.php @@ -0,0 +1,26 @@ + "GET", "logged_in" => true, "secure" => true]); + +$JobID = API::GetParameter("GET", "jobId", "string"); +$JobInfo = Database::singleton()->run("SELECT * FROM GameJobs WHERE JobID = :JobID", [":JobID" => $JobID])->fetch(\PDO::FETCH_OBJ); + +if (!$JobInfo) API::respond(200, false, "The requested game does not exist"); + +$ServerInfo = Database::singleton()->run("SELECT * FROM GameServers WHERE ServerID = :ServerID", [":ServerID" => $JobInfo->ServerID])->fetch(\PDO::FETCH_OBJ); +$PlaceInfo = Database::singleton()->run("SELECT * FROM assets WHERE id = :PlaceID", [":PlaceID" => $JobInfo->PlaceID])->fetch(\PDO::FETCH_OBJ); + +if ($PlaceInfo->creator != SESSION["user"]["id"] && !Users::IsAdmin(Users::STAFF_ADMINISTRATOR)) API::respond(200, false, "The requested game cannot be shut down"); +if ($JobInfo->Status == "Closed" || $JobInfo->Status == "Crashed") API::respond(200, false, "The requested game has already been shut down"); +if ($JobInfo->Status != "Ready") API::respond(200, false, "The requested game cannot be shut down"); + +$Request = "{\"Operation\":\"CloseJob\", \"JobID\":\"{$JobID}\"}"; +$Socket = fsockopen($ServerInfo->ServiceAddress, $ServerInfo->ServicePort); +fwrite($Socket, $Request); +fclose($Socket); + +API::respond(200, true, "The requested game has been shut down"); \ No newline at end of file diff --git a/api/groups/admin/get-members.php b/api/groups/admin/get-members.php new file mode 100644 index 0000000..457d374 --- /dev/null +++ b/api/groups/admin/get-members.php @@ -0,0 +1,55 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) API::respond(400, false, "Page is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$Page = $_POST["Page"] ?? 1; +$Members = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +$Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + +if($Rank->Level == 0) API::respond(200, false, "You are not a member of this group"); +if(!$Rank->Permissions->CanManageGroupAdmin) API::respond(200, false, "You are not allowed to perform this action"); + +$MemberCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_members + WHERE GroupID = :GroupID AND Rank < :RankLevel AND NOT Pending", + [":GroupID" => $GroupID, ":RankLevel" => $Rank->Level] +)->fetchColumn(); + +$Pages = ceil($MemberCount/12); +$Offset = ($Page - 1)*12; + +if(!$Pages) API::respond(200, true, "This group does not have any members."); + +$MembersQuery = Database::singleton()->run( + "SELECT users.username, users.id, Rank FROM groups_members + INNER JOIN users ON users.id = groups_members.UserID + WHERE GroupID = :GroupID AND Rank < :RankLevel AND NOT Pending + ORDER BY Joined DESC LIMIT 12 OFFSET $Offset", + [":GroupID" => $GroupID, ":RankLevel" => $Rank->Level] +); + +while($Member = $MembersQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Members[] = + [ + "UserName" => $Member->username, + "UserID" => $Member->id, + "RoleLevel" => $Member->Rank, + "Avatar" => Thumbnails::GetAvatar($Member->id) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "count" => $MemberCount, "items" => $Members])); \ No newline at end of file diff --git a/api/groups/admin/get-roles.php b/api/groups/admin/get-roles.php new file mode 100644 index 0000000..4b4de4e --- /dev/null +++ b/api/groups/admin/get-roles.php @@ -0,0 +1,47 @@ + "POST"]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$Roles = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +$Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + +if($Rank->Level == 0) API::respond(200, false, "You are not a member of this group"); +if(!$Rank->Permissions->CanManageGroupAdmin) API::respond(200, false, "You are not allowed to perform this action"); + +if($Rank->Level == 255) +{ + $RolesQuery = Database::singleton()->run( + "SELECT * FROM groups_ranks WHERE GroupID = :GroupID ORDER BY Rank ASC", + [":GroupID" => $GroupID] + ); +} +else +{ + $RolesQuery = Database::singleton()->run( + "SELECT * FROM groups_ranks WHERE GroupID = :GroupID AND Rank < :MyRank ORDER BY Rank ASC", + [":GroupID" => $GroupID, ":MyRank" => $Rank->Level] + ); +} + +while($Role = $RolesQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Roles[] = + [ + "Name" => htmlspecialchars($Role->Name), + "Description" => htmlspecialchars($Role->Description), + "Rank" => $Role->Rank, + "Permissions" => json_decode($Role->Permissions) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "items" => $Roles])); \ No newline at end of file diff --git a/api/groups/admin/request-relationship.php b/api/groups/admin/request-relationship.php new file mode 100644 index 0000000..8cd9d5a --- /dev/null +++ b/api/groups/admin/request-relationship.php @@ -0,0 +1,114 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Recipient"])) API::respond(400, false, "Recipient is not set"); + +if(!isset($_POST["Type"])) API::respond(400, false, "Type is not set"); +if(!in_array($_POST["Type"], ["ally", "enemy"])) API::respond(400, false, "Type is not valid"); + +$GroupID = $_POST["GroupID"] ?? false; +$RecipientName = $_POST["Recipient"] ?? false; +$Type = $_POST["Type"] ?? false; +$Groups = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +$Recipient = Database::singleton()->run("SELECT * FROM groups WHERE name = :GroupName", [":GroupName" => $RecipientName]); +$RecipientInfo = $Recipient->fetch(\PDO::FETCH_OBJ); + +if(!$Recipient->rowCount()) API::respond(200, false, "No group with that name exists"); + +$MyRank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); +if(!$MyRank->Permissions->CanManageRelationships) API::respond(200, false, "You are not allowed to manage this group's relationships"); + +if($RecipientInfo->id == $GroupID) +{ + if($Type == "ally") API::respond(200, false, "You cannot send an ally request to your own group"); + else if($Type == "enemy") API::respond(200, false, "You cannot declare your own group as an enemy"); +} + +$Relationship = Database::singleton()->run( + "SELECT * FROM groups_relationships WHERE :GroupID IN (Declarer, Recipient) AND :Recipient IN (Declarer, Recipient) AND Status != 2", + [":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id] +); +$RelationshipInfo = $Relationship->fetch(\PDO::FETCH_OBJ); + +if($Relationship->rowCount()) +{ + if($RelationshipInfo->Type == "Allies") + { + if($RelationshipInfo->Status == 0) + { + if($RelationshipInfo->Declarer == $GroupID) + { + API::respond(200, false, "You already have an outgoing ally request to this group"); + } + else + { + API::respond(200, false, "You already have an incoming ally request from this group"); + } + } + else if($RelationshipInfo->Status == 1) + { + API::respond(200, false, "You are already allies with this group!"); + } + } + else if($RelationshipInfo->Type == "Enemies") + { + API::respond(200, false, "You are already enemies with this group!"); + } +} + +if($Type == "ally") +{ + $LastRequest = Database::singleton()->run("SELECT Declared FROM groups_relationships WHERE Declarer = :GroupID AND Declared+3600 > UNIX_TIMESTAMP()", [":GroupID" => $GroupID]); + if($LastRequest->rowCount()) + API::respond(429, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 hour"])." before sending a new ally request"); + + Database::singleton()->run( + "INSERT INTO groups_relationships (Type, Declarer, Recipient, Status, Declared) + VALUES (\"Allies\", :GroupID, :Recipient, 0, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id] + ); + + Groups::LogAction( + $GroupID, "Send Ally Request", + sprintf( + "%s sent an ally request to %s", + SESSION["user"]["id"], SESSION["user"]["username"], $RecipientInfo->id, htmlspecialchars($RecipientInfo->name) + ) + ); + API::respond(200, true, "Ally request has been sent to ".Polygon::FilterText($RecipientInfo->name)); +} +else if($Type == "enemy") +{ + $LastRequest = Database::singleton()->run("SELECT Declared FROM groups_relationships WHERE Declarer = :GroupID AND Declared+3600 > UNIX_TIMESTAMP()", [":GroupID" => $GroupID]); + if($LastRequest->rowCount()) + API::respond(429, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 hour"])." before sending a new ally request"); + + Database::singleton()->run( + "INSERT INTO groups_relationships (Type, Declarer, Recipient, Status, Declared, Established) + VALUES (\"Enemies\", :GroupID, :Recipient, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id] + ); + + Groups::LogAction( + $GroupID, "Create Enemy", + sprintf( + "%s declared %s as an enemy", + SESSION["user"]["id"], SESSION["user"]["username"], $RecipientInfo->id, htmlspecialchars($RecipientInfo->name) + ) + ); + API::respond(200, true, Polygon::FilterText($RecipientInfo->name)." is now your enemy!"); +} + +API::respond(200, false, "An unexpected error occurred"); \ No newline at end of file diff --git a/api/groups/admin/update-member.php b/api/groups/admin/update-member.php new file mode 100644 index 0000000..8265409 --- /dev/null +++ b/api/groups/admin/update-member.php @@ -0,0 +1,61 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["UserID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["UserID"])) API::respond(400, false, "GroupID is not a number"); + +if(isset($_POST["RoleLevel"]) && !is_numeric($_POST["RoleLevel"])) API::respond(400, false, "RoleLevel is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$UserID = $_POST["UserID"] ?? false; + +$RoleLevel = $_POST["RoleLevel"] ?? false; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +$MyRole = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); +$UserRole = Groups::GetUserRank($UserID, $GroupID); + +if($MyRole->Level == 0) API::respond(200, false, "You are not a member of this group"); +if(!$MyRole->Permissions->CanManageGroupAdmin) API::respond(200, false, "You are not allowed to perform this action"); +if($UserRole->Level == 0) API::respond(200, false, "That user is not a member of this group"); + +if($RoleLevel !== false) +{ + if(!Groups::GetRankInfo($GroupID, $RoleLevel)) API::respond(200, false, "That role does not exist"); + if($RoleLevel == 0 || $RoleLevel == 255) API::respond(200, false, "That role cannot be manually assigned to a member"); + + if($UserRole->Level == $RoleLevel) API::respond(200, false, "The role you tried to assign is the user's current role"); + if($MyRole->Level <= $RoleLevel) API::respond(200, false, "You can only assign roles lower than yours"); + if($MyRole->Level <= $UserRole->Level) API::respond(200, false, "You can only modify the role of a user who is a role lower than yours"); + + Database::singleton()->run( + "UPDATE groups_members SET Rank = :RoleLevel WHERE GroupID = :GroupID AND UserID = :UserID", + [":GroupID" => $GroupID, ":UserID" => $UserID, ":RoleLevel" => $RoleLevel] + ); + + $UserName = Users::GetNameFromID($UserID); + $RoleName = Groups::GetRankInfo($GroupID, $RoleLevel)->Name; + $Action = $RoleLevel > $UserRole->Level ? "promoted" : "demoted"; + + Groups::LogAction( + $GroupID, "Change Rank", + sprintf( + "%s %s %s from %s to %s", + SESSION["user"]["id"], SESSION["user"]["username"], $Action, $UserID, $UserName, htmlspecialchars($UserRole->Name), htmlspecialchars($RoleName) + ) + ); + + API::respond(200, true, "$UserName has been $Action to " . htmlspecialchars($RoleName)); +} + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/admin/update-relationship.php b/api/groups/admin/update-relationship.php new file mode 100644 index 0000000..a112268 --- /dev/null +++ b/api/groups/admin/update-relationship.php @@ -0,0 +1,108 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Recipient"])) API::respond(400, false, "Recipient is not set"); +if(!is_numeric($_POST["Recipient"])) API::respond(400, false, "Recipient is not a number"); + +if(!isset($_POST["Action"])) API::respond(400, false, "Action is not set"); +if(!in_array($_POST["Action"], ["accept", "decline"])) API::respond(400, false, "Action is not valid"); + +$GroupID = $_POST["GroupID"] ?? false; +$Recipient = $_POST["Recipient"] ?? false; +$Action = $_POST["Action"] ?? false; +$Groups = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +if(!Groups::GetGroupInfo($Recipient)) API::respond(200, false, "Recipient group does not exist"); + +$MyRank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); +if(!$MyRank->Permissions->CanManageRelationships) API::respond(200, false, "You are not allowed to manage this group's relationships"); + +$Relationship = Database::singleton()->run( + "SELECT groups_relationships.*, groups.name FROM groups_relationships + INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END) + WHERE :GroupID IN (Declarer, Recipient) AND :Recipient IN (Declarer, Recipient) AND Status != 2", + [":GroupID" => $GroupID, ":Recipient" => $Recipient] +); +$RelationshipInfo = $Relationship->fetch(\PDO::FETCH_OBJ); + +if(!$Relationship->rowCount()) API::respond(200, false, "You are not in a relationship with this group"); + +if($Action == "accept") +{ + if($RelationshipInfo->Type == "Enemies") API::respond(200, false, "You cannot accept an enemy relationship"); + if($RelationshipInfo->Status != 0) API::respond(200, false, "You are already in a relationship with this group"); + + Database::singleton()->run( + "UPDATE groups_relationships SET Status = 1, Established = UNIX_TIMESTAMP() WHERE ID = :RelationshipID", + [":RelationshipID" => $RelationshipInfo->ID] + ); + + Groups::LogAction( + $GroupID, "Accept Ally Request", + sprintf( + "%s accepted an ally request from %s", + SESSION["user"]["id"], SESSION["user"]["username"], $Recipient, htmlspecialchars($RelationshipInfo->name) + ) + ); + + API::respond(200, true, "You have accepted {$RelationshipInfo->name}'s ally request"); +} +else if($Action == "decline") +{ + Database::singleton()->run( + "UPDATE groups_relationships SET Status = 2, Broken = UNIX_TIMESTAMP() WHERE ID = :RelationshipID", + [":RelationshipID" => $RelationshipInfo->ID] + ); + + if($RelationshipInfo->Type == "Allies") + { + if($RelationshipInfo->Status == 0) + { + Groups::LogAction( + $GroupID, "Decline Ally Request", + sprintf( + "%s declined an ally request from %s", + SESSION["user"]["id"], SESSION["user"]["username"], $Recipient, htmlspecialchars($RelationshipInfo->name) + ) + ); + + API::respond(200, true, "You have declined ".Polygon::FilterText($RelationshipInfo->name)."'s ally request"); + } + else if($RelationshipInfo->Status == 1) + { + Groups::LogAction( + $GroupID, "Delete Ally", + sprintf( + "%s removed %s as an ally", + SESSION["user"]["id"], SESSION["user"]["username"], $Recipient, htmlspecialchars($RelationshipInfo->name) + ) + ); + + API::respond(200, true, "You are no longer allies with ".Polygon::FilterText($RelationshipInfo->name)); + } + } + else if($RelationshipInfo->Type == "Enemies") + { + Groups::LogAction( + $GroupID, "Delete Enemy", + sprintf( + "%s removed %s as an enemy", + SESSION["user"]["id"], SESSION["user"]["username"], $Recipient, htmlspecialchars($RelationshipInfo->name) + ) + ); + + API::respond(200, true, "You are no longer enemies with ".Polygon::FilterText($RelationshipInfo->name)); + } +} + +API::respond(200, false, "An unexpected error occurred"); \ No newline at end of file diff --git a/api/groups/admin/update-roles.php b/api/groups/admin/update-roles.php new file mode 100644 index 0000000..cc8544c --- /dev/null +++ b/api/groups/admin/update-roles.php @@ -0,0 +1,170 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Roles"])) API::respond(400, false, "Roles is not set"); + +$GroupID = $_POST["GroupID"]; +$Roles = json_decode($_POST["Roles"]); + +if(!$Roles) API::respond(400, false, "Roles is not valid JSON"); +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +$MyRole = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + +if($MyRole->Level == 0) API::respond(200, false, "You are not a member of this group"); +if($MyRole->Level != 255) API::respond(200, false, "You are not allowed to perform this action"); + +function FindRolesWithRank($Rank) +{ + global $Roles; + $Count = 0; + + foreach ($Roles as $Role) + { + if (!isset($Role->Rank)) continue; + if ($Role->Rank == $Rank) $Count += 1; + } + + return $Count; +} + +function FindRoleWithRank($Rank) +{ + global $Roles; + + foreach ($Roles as $Role) + { + if (!isset($Role->Rank)) continue; + if ($Role->Rank == $Rank) return $Role; + } + + return false; +} + +$Permissions = +[ + "CanViewGroupWall", + "CanViewGroupStatus", + "CanPostOnGroupWall", + "CanPostGroupStatus", + "CanDeleteGroupWallPosts", + "CanAcceptJoinRequests", + "CanKickLowerRankedMembers", + "CanRoleLowerRankedMembers", + "CanManageRelationships", + "CanCreateAssets", + "CanConfigureAssets", + "CanSpendFunds", + "CanManageGames", + "CanManageGroupAdmin", + "CanViewAuditLog" +]; + +if(FindRolesWithRank(0) == 0) API::respond(200, false, "You can not remove the Guest role"); +if(FindRolesWithRank(255) == 0) API::respond(200, false, "You can not remove the Owner role"); +if(count($Roles) < 3) API::respond(200, false, "There must be at least three roles"); +if(count($Roles) > 10) API::respond(200, false, "There must be no more than ten roles"); + +foreach($Roles as $Role) +{ + if(!isset($Role->Name) || !isset($Role->Description) || !isset($Role->Rank) || !isset($Role->Permissions)) + API::respond(200, false, "Roles are missing parameters"); + + if($Role->Rank < 0 || $Role->Rank > 255) API::respond(200, false, "Each role must have a rank number between 0 and 255"); + if(FindRolesWithRank($Role->Rank) > 1) API::respond(200, false, "Each role must have a unique rank number"); + + $CurrentRole = Groups::GetRankInfo($GroupID, $Role->Rank); + + if($CurrentRole === false) $Role->Action = "Create"; + else $Role->Action = "Update"; + + if($Role->Rank == 0) + { + if($Role->Name != $CurrentRole->Name || $Role->Description != $CurrentRole->Description) + API::respond(200, false, "You can not modify the Guest role"); + } + + if($Role->Rank == 255 && $Role->Permissions != $CurrentRole->Permissions) + API::respond(200, false, "You can not modify the permissions of the Owner role"); + + if(strlen($Role->Name) < 3) API::respond(200, false, "Role names must be at least 3 characters long"); + if(strlen($Role->Name) > 15) API::respond(200, false, "Role names must be no longer than 15 characters"); + + if(strlen($Role->Description) < 3) API::respond(200, false, "Role description must at least 3 characters long"); + if(strlen($Role->Description) > 64) API::respond(200, false, "Role description must be no longer than 64 characters"); + + foreach ($Permissions as $Permission) + { + if(!isset($Role->Permissions->$Permission)) API::respond(200, false, "Role is missing a permission"); + if(!is_bool($Role->Permissions->$Permission)) API::respond(200, false, "Role permission property must have a boolean value"); + } + + if(count((array)$Role->Permissions) != count($Permissions)) API::respond(200, false, "Role permissions contains an incorrect number of permissions"); +} + +foreach($Roles as $Role) +{ + if($Role->Action == "Create") + { + // if(SESSION["user"]["id"] == 1) echo "Creating Role {$Role->Rank}\r\n"; + + Database::singleton()->run( + "INSERT INTO groups_ranks (GroupID, Name, Description, Rank, Permissions, Created) + VALUES (:GroupID, :Name, :Description, :Rank, :Permissions, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":Name" => $Role->Name, ":Description" => $Role->Description, ":Rank" => $Role->Rank, ":Permissions" => json_encode($Role->Permissions)] + ); + } +} + +$GroupRoles = Groups::GetGroupRanks($GroupID, true); +while($ExistingRole = $GroupRoles->fetch(\PDO::FETCH_OBJ)) +{ + $Role = FindRoleWithRank($ExistingRole->Rank); + + if($Role == false) + { + // if(SESSION["user"]["id"] == 1) echo "Deleting Role {$ExistingRole->Rank}\r\n"; + + // for this one we gotta move the members with a role thats being deleted to the lowest rank + // slight issue with this is for a brief period, members assigned the role thats being deleted + // will have no role - if the timing is just right this could mess up the view of the group page + + // delete the rank by the oldest id - so that we dont accidentally delete the new one + Database::singleton()->run( + "DELETE FROM groups_ranks WHERE GroupID = :GroupID AND Rank = :Rank ORDER BY id ASC LIMIT 1", + [":GroupID" => $GroupID, ":Rank" => $ExistingRole->Rank] + ); + + $NewRank = Database::singleton()->run( + "SELECT Rank FROM polygon.groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1", + [":GroupID" => $GroupID] + )->fetchColumn(); + + // if(SESSION["user"]["id"] == 1) echo "Updating existing members to {$NewRank}\r\n"; + + Database::singleton()->run( + "UPDATE groups_members SET Rank = :NewRank WHERE GroupID = :GroupID AND Rank = :Rank", + [":GroupID" => $GroupID, ":Rank" => $ExistingRole->Rank, ":NewRank" => $NewRank] + ); + } + else if(isset($Role->Action) && $Role->Action == "Update") + { + // if(SESSION["user"]["id"] == 1) echo "Updating Role {$Role->Rank}\r\n"; + + Database::singleton()->run( + "UPDATE groups_ranks SET Name = :Name, Description = :Description, Permissions = :Permissions + WHERE GroupID = :GroupID AND Rank = :Rank", + [":GroupID" => $GroupID, ":Name" => $Role->Name, ":Description" => $Role->Description, ":Rank" => $Role->Rank, ":Permissions" => json_encode($Role->Permissions)] + ); + } +} + +die(json_encode(["status" => 200, "success" => true, "message" => "Group roles have successfully been updated"])); \ No newline at end of file diff --git a/api/groups/delete-wall-post.php b/api/groups/delete-wall-post.php new file mode 100644 index 0000000..37f1e12 --- /dev/null +++ b/api/groups/delete-wall-post.php @@ -0,0 +1,44 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["PostID"])) API::respond(400, false, "PostID is not set"); +if(!is_numeric($_POST["PostID"])) API::respond(400, false, "PostID is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$PostID = $_POST["PostID"] ?? false; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +$Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); +if(!$Rank->Permissions->CanDeleteGroupWallPosts) API::respond(200, false, "You are not allowed to delete wall posts on this group"); + +$PostInfo = Database::singleton()->run( + "SELECT * FROM groups_wall WHERE id = :PostID AND :GroupID = :GroupID", + [":PostID" => $PostID, ":GroupID" => $GroupID] +)->fetch(\PDO::FETCH_OBJ); + +if(!$PostInfo) API::respond(200, false, "Wall post does not exist"); + +Groups::LogAction( + $GroupID, "Delete Post", + sprintf( + "%s deleted post \"%s\" by %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($PostInfo->Content), $PostInfo->PosterID, Users::GetNameFromID($PostInfo->PosterID) + ) +); + +Database::singleton()->run( + "DELETE FROM groups_wall WHERE id = :PostID AND :GroupID = :GroupID", + [":PostID" => $PostID, ":GroupID" => $GroupID] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/get-audit.php b/api/groups/get-audit.php new file mode 100644 index 0000000..e858dd7 --- /dev/null +++ b/api/groups/get-audit.php @@ -0,0 +1,79 @@ + "POST"]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) API::respond(400, false, "Page is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$Filter = $_POST["Filter"] ?? "All Actions"; +$Page = $_POST["Page"] ?? 1; +$Logs = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +$MyRank = Groups::GetUserRank(SESSION["user"]["id"] ?? 0, $GroupID); +if(!$MyRank->Permissions->CanViewAuditLog) API::respond(200, false, "You cannot audit this group"); + +if($Filter == "All Actions") +{ + $LogCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_audit WHERE GroupID = :GroupID", + [":GroupID" => $GroupID] + )->fetchColumn(); +} +else +{ + $LogCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_audit WHERE GroupID = :GroupID AND Category = :Action", + [":GroupID" => $GroupID, ":Action" => $Filter] + )->fetchColumn(); +} + +$Pages = ceil($LogCount/20); +$Offset = ($Page - 1)*20; + +if(!$Pages) API::respond(200, true, "This group does not have any logs for this action."); + +if($Filter == "All Actions") +{ + $LogsQuery = Database::singleton()->run( + "SELECT groups_audit.*, users.username FROM groups_audit + INNER JOIN users ON users.id = UserId + WHERE GroupID = :GroupID + ORDER BY Time DESC LIMIT 20 OFFSET $Offset", + [":GroupID" => $GroupID] + ); +} +else +{ + $LogsQuery = Database::singleton()->run( + "SELECT groups_audit.*, users.username FROM groups_audit + INNER JOIN users ON users.id = UserId + WHERE GroupID = :GroupID AND Category = :Action + ORDER BY Time DESC LIMIT 20 OFFSET $Offset", + [":GroupID" => $GroupID, ":Action" => $Filter] + ); +} + +while($Log = $LogsQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Logs[] = + [ + "Date" => date('j/n/y G:i', $Log->Time), + "UserName" => $Log->username, + "UserID" => $Log->UserID, + "UserAvatar" => Thumbnails::GetAvatar($Log->UserID), + "Rank" => Polygon::FilterText($Log->Rank), + "Description" => Polygon::FilterText($Log->Description, false) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Logs])); \ No newline at end of file diff --git a/api/groups/get-members.php b/api/groups/get-members.php new file mode 100644 index 0000000..2199ba8 --- /dev/null +++ b/api/groups/get-members.php @@ -0,0 +1,53 @@ + "POST"]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["RankLevel"])) API::respond(400, false, "RankLevel is not set"); +if(!is_numeric($_POST["RankLevel"])) API::respond(400, false, "RankLevel is not a number"); + +if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) API::respond(400, false, "Page is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$RankID = $_POST["RankLevel"] ?? false; +$Members = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +if(!Groups::GetRankInfo($GroupID, $RankID)) API::respond(200, false, "Group rank does not exist"); + +$MemberCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_members WHERE GroupID = :GroupID AND Rank = :RankID AND NOT Pending", + [":GroupID" => $GroupID, ":RankID" => $RankID] +)->fetchColumn(); + +$Pagination = Pagination($_POST["Page"] ?? 1, $MemberCount, 12); + +if($Pagination->Pages == 0) API::respond(200, true, "This group does not have any members of this rank."); + +$MembersQuery = Database::singleton()->run( + "SELECT users.username, users.id, Rank FROM groups_members + INNER JOIN users ON users.id = groups_members.UserID + WHERE GroupID = :GroupID AND Rank = :RankID AND NOT Pending + ORDER BY Joined DESC LIMIT 12 OFFSET :Offset", + [":GroupID" => $GroupID, ":RankID" => $RankID, ":Offset" => $Pagination->Offset] +); + +while($Member = $MembersQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Members[] = + [ + "UserName" => $Member->username, + "UserID" => $Member->id, + "RoleLevel" => $Member->Rank, + "Avatar" => Thumbnails::GetAvatar($Member->id) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "count" => $MemberCount, "items" => $Members])); \ No newline at end of file diff --git a/api/groups/get-related.php b/api/groups/get-related.php new file mode 100644 index 0000000..d5f4493 --- /dev/null +++ b/api/groups/get-related.php @@ -0,0 +1,82 @@ + "POST"]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Type"])) API::respond(400, false, "Type is not set"); +if(!in_array($_POST["Type"], ["Pending Allies", "Allies", "Enemies"])) API::respond(400, false, "Type is not valid"); + +if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) API::respond(400, false, "Page is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$Type = $_POST["Type"] ?? false; +$Page = $_POST["Page"] ?? 1; +$Groups = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +if($Type == "Pending Allies") +{ + if(!SESSION) API::respond(200, false, "You are not allowed to get this group's pending allies"); + $MyRank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + if(!$MyRank->Permissions->CanManageRelationships) API::respond(200, false, "You are not allowed to get this group's pending allies"); + + $GroupsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_relationships WHERE Recipient = :GroupID AND Type = \"Allies\" AND Status = 0", + [":GroupID" => $GroupID] + )->fetchColumn(); +} +else +{ + $GroupsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_relationships WHERE :GroupID IN (Declarer, Recipient) AND Type = :Type AND Status = 1", + [":GroupID" => $GroupID, ":Type" => $Type] + )->fetchColumn(); +} + +$Pages = ceil($GroupsCount/12); +$Offset = ($Page - 1)*12; + +if(!$Pages) API::respond(200, true, "This group does not have any $Type"); + +if($Type == "Pending Allies") +{ + $GroupsQuery = Database::singleton()->run( + "SELECT groups.name, groups.id, groups.emblem, groups.MemberCount FROM groups_relationships + INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END) + WHERE Recipient = :GroupID AND Type = \"Allies\" AND Status = 0 + ORDER BY Declared DESC LIMIT 12 OFFSET $Offset", + [":GroupID" => $GroupID] + ); +} +else +{ + $GroupsQuery = Database::singleton()->run( + "SELECT groups.name, groups.id, groups.emblem, groups.MemberCount FROM groups_relationships + INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END) + WHERE :GroupID IN (Declarer, Recipient) AND Type = :Type AND Status = 1 + ORDER BY Established DESC LIMIT 12 OFFSET $Offset", + [":GroupID" => $GroupID, ":Type" => $Type] + ); +} + +while($Group = $GroupsQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Groups[] = + [ + "Name" => Polygon::FilterText($Group->name), + "ID" => $Group->id, + "MemberCount" => $Group->MemberCount, + "Emblem" => Thumbnails::GetAssetFromID($Group->emblem) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Groups])); \ No newline at end of file diff --git a/api/groups/get-wall.php b/api/groups/get-wall.php new file mode 100644 index 0000000..c9117d7 --- /dev/null +++ b/api/groups/get-wall.php @@ -0,0 +1,55 @@ + "POST"]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) API::respond(400, false, "Page is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$Wall = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +if(SESSION) $Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); +else $Rank = Groups::GetRankInfo($GroupID, 0); + +if(!$Rank->Permissions->CanViewGroupWall) API::respond(200, false, "You are not allowed to view this group wall"); + +$PostCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_wall WHERE GroupID = :GroupID AND NOT Deleted", + [":GroupID" => $GroupID] +)->fetchColumn(); + +$Pagination = Pagination($_POST["Page"] ?? 1, $PostCount, 15); + +if($Pagination->Pages == 0) API::respond(200, true, "This group does not have any wall posts."); + +$PostQuery = Database::singleton()->run( + "SELECT groups_wall.id, users.username AS PosterName, PosterID, Content, TimePosted FROM groups_wall + INNER JOIN users ON users.id = PosterID WHERE GroupID = :GroupID AND NOT Deleted + ORDER BY TimePosted DESC LIMIT 15 OFFSET :Offset", + [":GroupID" => $GroupID, ":Offset" => $Pagination->Offset] +); + +while($Post = $PostQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Wall[] = + [ + "id" => $Post->id, + "username" => $Post->PosterName, + "userid" => $Post->PosterID, + "content" => nl2br(Polygon::FilterText($Post->Content)), + "time" => date('j/n/Y g:i:s A', $Post->TimePosted), + "avatar" => Thumbnails::GetAvatar($Post->PosterID) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "count" => $PostCount, "items" => $Wall])); \ No newline at end of file diff --git a/api/groups/join-group.php b/api/groups/join-group.php new file mode 100644 index 0000000..35a7450 --- /dev/null +++ b/api/groups/join-group.php @@ -0,0 +1,35 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); +if(Groups::CheckIfUserInGroup(SESSION["user"]["id"], $GroupID)) API::respond(200, false, "You are already in this group"); + +if(Groups::GetUserGroups(SESSION["user"]["id"])->rowCount() >= 20) API::respond(200, false, "You have reached the maximum number of groups"); + +$RateLimit = Database::singleton()->run("SELECT Joined FROM groups_members WHERE UserID = :UserID AND Joined+300 > UNIX_TIMESTAMP()", [":UserID" => SESSION["user"]["id"]]); +if($RateLimit->rowCount()) + API::respond(200, false, "Please wait ".GetReadableTime($RateLimit->fetchColumn(), ["RelativeTime" => "5 minutes"])." before joining a new group"); + +$RankLevel = Database::singleton()->run( + "SELECT Rank FROM groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1", + [":GroupID" => $GroupID] +)->fetchColumn(); + +Database::singleton()->run( + "INSERT INTO groups_members (GroupID, UserID, Rank, Joined) VALUES (:GroupID, :UserID, :RankLevel, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":RankLevel" => $RankLevel] +); + +Database::singleton()->run("UPDATE groups SET MemberCount = MemberCount + 1 WHERE id = :GroupID", [":GroupID" => $GroupID]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/leave-group.php b/api/groups/leave-group.php new file mode 100644 index 0000000..3a865e5 --- /dev/null +++ b/api/groups/leave-group.php @@ -0,0 +1,26 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +$GroupID = $_POST["GroupID"] ?? false; +$GroupInfo = Groups::GetGroupInfo($GroupID); + +if(!$GroupInfo) API::respond(200, false, "Group does not exist"); +if($GroupInfo->creator == SESSION["user"]["id"]) API::respond(200, false, "You are the creator of this group"); +if(!Groups::CheckIfUserInGroup(SESSION["user"]["id"], $GroupID)) API::respond(200, false, "You are not in this group"); + +Database::singleton()->run( + "DELETE FROM groups_members WHERE GroupID = :GroupID AND UserID = :UserID", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] +); + +Database::singleton()->run("UPDATE groups SET MemberCount = MemberCount - 1 WHERE id = :GroupID", [":GroupID" => $GroupID]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/post-shout.php b/api/groups/post-shout.php new file mode 100644 index 0000000..43d0017 --- /dev/null +++ b/api/groups/post-shout.php @@ -0,0 +1,59 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Content"])) API::respond(400, false, "Content is not set"); + +$GroupID = $_POST["GroupID"] ?? false; +$Content = $_POST["Content"] ?? false; +$GroupInfo = Groups::GetGroupInfo($GroupID); + +if(!$GroupInfo) API::respond(200, false, "Group does not exist"); + +$Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + +if(!$Rank->Permissions->CanPostGroupStatus) API::respond(200, false, "You are not allowed to post on this group wall"); + +if(strlen($Content) < 3) API::respond(200, false, "Group shout must be at least 3 characters long"); +if(strlen($Content) > 255) API::respond(200, false, "Group shout can not be longer than 64 characters"); + +$LastPost = Database::singleton()->run( + "SELECT timestamp FROM feed WHERE groupId = :GroupID AND userId = :UserID AND timestamp+300 > UNIX_TIMESTAMP()", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] +); + +if($LastPost->rowCount()) + API::respond(200, false, "Please wait ".GetReadableTime($LastPost->fetchColumn(), ["RelativeTime" => "5 minutes"])." before posting a group shout"); + +Groups::LogAction( + $GroupID, "Post Shout", + sprintf( + "%s changed the group status to: %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($Content) + ) +); + +Database::singleton()->run( + "INSERT INTO feed (groupId, userId, text, timestamp) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":Content" => $Content] +); + +Discord::SendToWebhook( + [ + "username" => $GroupInfo->name, + "content" => $Content."\n(Posted by ".SESSION["user"]["username"].")", + "avatar_url" => Thumbnails::GetAssetFromID($GroupInfo->emblem) + ], + Discord::WEBHOOK_KUSH +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/post-wall.php b/api/groups/post-wall.php new file mode 100644 index 0000000..a8cebb8 --- /dev/null +++ b/api/groups/post-wall.php @@ -0,0 +1,41 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST["GroupID"])) API::respond(400, false, "GroupID is not set"); +if(!is_numeric($_POST["GroupID"])) API::respond(400, false, "GroupID is not a number"); + +if(!isset($_POST["Content"])) API::respond(400, false, "Content is not set"); + +$GroupID = $_POST["GroupID"] ?? false; +$Content = $_POST["Content"] ?? false; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +$Rank = Groups::GetUserRank(SESSION["user"]["id"], $GroupID); + +if(!$Rank->Permissions->CanPostOnGroupWall) API::respond(200, false, "You are not allowed to post on this group wall"); + +if(strlen($Content) < 3) API::respond(200, false, "Wall post must be at least 3 characters long"); +if(strlen($Content) > 255) API::respond(200, false, "Wall post can not be longer than 255 characters"); + +$LastPost = Database::singleton()->run( + "SELECT TimePosted FROM groups_wall + WHERE GroupID = :GroupID AND PosterID = :UserID AND TimePosted+60 > UNIX_TIMESTAMP() + ORDER BY TimePosted DESC LIMIT 1", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] +); + +if(SESSION["user"]["id"] != 1 && $LastPost->rowCount()) + API::respond(200, false, "Please wait ".(60-(time()-$LastPost->fetchColumn()))." seconds before posting a new wall post"); + +Database::singleton()->run( + "INSERT INTO groups_wall (GroupID, PosterID, Content, TimePosted) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":Content" => $Content] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/ide/toolbox.php b/api/ide/toolbox.php new file mode 100644 index 0000000..8dccccc --- /dev/null +++ b/api/ide/toolbox.php @@ -0,0 +1,121 @@ +"Bricks", + 1=>"Robots", + 2=>"Chassis", + 3=>"Furniture", + 4=>"Roads", + 5=>"Billboards", + 6=>"Game Objects", + "MyDecals"=>"My Decals", + "FreeDecals"=>"Free Decals", + "MyModels"=>"My Models", + "FreeModels"=>"Free Models" +]; + +$category = isset($_POST['category']) && isset($categories[$_POST['category']]) ? $_POST['category'] : "FreeModels"; +$categoryText = $categories[$category]; +$type = strpos($category, "Decals") ? 13 : 10; +$page = $_POST['page'] ?? 1; +$keywd = $_POST['keyword'] ?? false; + +if(is_numeric($category)) //static category +{ + //$query = $pdo->prepare("SELECT COUNT(*) FROM catalog_items WHERE toolboxCategory = :category"); + //$query->bindParam(":category", $categoryText, \PDO::PARAM_STR); +} +else //dynamic category - user assets, catalog assets +{ + if(SESSION && strpos($categoryText, "My") !== false) //get assets from inventory + { + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND id IN (SELECT assetId FROM ownedAssets WHERE userId = :uid)", + [":uid" => SESSION["user"]["id"], ":type" => $type] + )->fetchColumn(); + } + else //get assets from catalog + { + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q)", + [":type" => $type, ":q" => "%{$keywd}%"] + )->fetchColumn(); + } +} + +$pagination = Pagination($page, $assetCount, 20); + +if(is_numeric($category)) //static category +{ + //$query = $pdo->prepare("SELECT * FROM catalog_items WHERE toolboxCategory = :category ORDER BY id ASC LIMIT 20 OFFSET :offset"); + //$query->bindParam(":category", $categoryText, \PDO::PARAM_STR); +} +else //dynamic category - user assets, catalog assets +{ + if(strpos($categoryText, "My") !== false) //get assets from inventory + { + $assets = Database::singleton()->run( + "SELECT assets.* FROM ownedAssets + INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND assets.type = :type + ORDER BY timestamp DESC LIMIT 20 OFFSET :offset", + [":uid" => SESSION["user"]["id"], ":type" => $type, ":offset" => $pagination->Offset] + ); + } + else //get assets from catalog + { + $assets = Database::singleton()->run( + "SELECT * FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q) + ORDER BY updated DESC LIMIT 20 OFFSET :offset", + [":type" => $type, ":q" => "%{$keywd}%", ":offset" => $pagination->Offset] + ); + } +} +?> +', + 'handler' => 'raw', + ), + ); + } + else if (strcasecmp($Link['element']['text'], 'YouTube') == 0) + { + parse_str(parse_url($Link['element']['attributes']['href'])["query"], $params); + + if (!isset($params['v'])) + return; + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'div', + 'attributes' => array( + 'class' => 'embed-responsive embed-responsive-16by9', + ), + 'text' => '', + 'handler' => 'raw', + ), + ); + } + else + { + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'class' => "img-fluid", + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + } +/// PROJECT POLYGON /// + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "
"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php b/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php new file mode 100644 index 0000000..c248ec0 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php @@ -0,0 +1,292 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\GoogleAuthenticator; + +/** + * FixedBitNotation. + * + * The FixedBitNotation class is for binary to text conversion. It + * can handle many encoding schemes, formally defined or not, that + * use a fixed number of bits to encode each character. + * + * @author Andre DeMarre + */ +final class FixedBitNotation +{ + /** + * @var string + */ + private $chars; + + /** + * @var int + */ + private $bitsPerCharacter; + + /** + * @var int + */ + private $radix; + + /** + * @var bool + */ + private $rightPadFinalBits; + + /** + * @var bool + */ + private $padFinalGroup; + + /** + * @var string + */ + private $padCharacter; + + /** + * @var string[] + */ + private $charmap; + + /** + * @param int $bitsPerCharacter Bits to use for each encoded character + * @param string $chars Base character alphabet + * @param bool $rightPadFinalBits How to encode last character + * @param bool $padFinalGroup Add padding to end of encoded output + * @param string $padCharacter Character to use for padding + */ + public function __construct(int $bitsPerCharacter, ?string $chars = null, bool $rightPadFinalBits = false, bool $padFinalGroup = false, string $padCharacter = '=') + { + // Ensure validity of $chars + if (!\is_string($chars) || ($charLength = \strlen($chars)) < 2) { + $chars = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,'; + $charLength = 64; + } + + // Ensure validity of $bitsPerCharacter + if ($bitsPerCharacter < 1) { + // $bitsPerCharacter must be at least 1 + $bitsPerCharacter = 1; + $radix = 2; + } elseif ($charLength < 1 << $bitsPerCharacter) { + // Character length of $chars is too small for $bitsPerCharacter + // Set $bitsPerCharacter to greatest acceptable value + $bitsPerCharacter = 1; + $radix = 2; + + while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) { + ++$bitsPerCharacter; + } + + $radix >>= 1; + } elseif ($bitsPerCharacter > 8) { + // $bitsPerCharacter must not be greater than 8 + $bitsPerCharacter = 8; + $radix = 256; + } else { + $radix = 1 << $bitsPerCharacter; + } + + $this->chars = $chars; + $this->bitsPerCharacter = $bitsPerCharacter; + $this->radix = $radix; + $this->rightPadFinalBits = $rightPadFinalBits; + $this->padFinalGroup = $padFinalGroup; + $this->padCharacter = $padCharacter[0]; + } + + /** + * Encode a string. + * + * @param string $rawString Binary data to encode + */ + public function encode($rawString): string + { + // Unpack string into an array of bytes + $bytes = unpack('C*', $rawString); + $byteCount = \count($bytes); + + $encodedString = ''; + $byte = array_shift($bytes); + $bitsRead = 0; + + $chars = $this->chars; + $bitsPerCharacter = $this->bitsPerCharacter; + $rightPadFinalBits = $this->rightPadFinalBits; + $padFinalGroup = $this->padFinalGroup; + $padCharacter = $this->padCharacter; + + // Generate encoded output; + // each loop produces one encoded character + for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; ++$c) { + // Get the bits needed for this encoded character + if ($bitsRead + $bitsPerCharacter > 8) { + // Not enough bits remain in this byte for the current + // character + // Save the remaining bits before getting the next byte + $oldBitCount = 8 - $bitsRead; + $oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount); + $newBitCount = $bitsPerCharacter - $oldBitCount; + + if (!$bytes) { + // Last bits; match final character and exit loop + if ($rightPadFinalBits) { + $oldBits <<= $newBitCount; + } + $encodedString .= $chars[$oldBits]; + + if ($padFinalGroup) { + // Array of the lowest common multiples of + // $bitsPerCharacter and 8, divided by 8 + $lcmMap = [1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1]; + $bytesPerGroup = $lcmMap[$bitsPerCharacter]; + $pads = (int) ($bytesPerGroup * 8 / $bitsPerCharacter + - ceil((\strlen($rawString) % $bytesPerGroup) + * 8 / $bitsPerCharacter)); + $encodedString .= str_repeat($padCharacter[0], $pads); + } + + break; + } + + // Get next byte + $byte = array_shift($bytes); + $bitsRead = 0; + } else { + $oldBitCount = 0; + $newBitCount = $bitsPerCharacter; + } + + // Read only the needed bits from this byte + $bits = $byte >> 8 - ($bitsRead + $newBitCount); + $bits ^= $bits >> $newBitCount << $newBitCount; + $bitsRead += $newBitCount; + + if ($oldBitCount) { + // Bits come from seperate bytes, add $oldBits to $bits + $bits = ($oldBits << $newBitCount) | $bits; + } + + $encodedString .= $chars[$bits]; + } + + return $encodedString; + } + + /** + * Decode a string. + * + * @param string $encodedString Data to decode + * @param bool $caseSensitive + * @param bool $strict Returns null if $encodedString contains + * an undecodable character + */ + public function decode($encodedString, $caseSensitive = true, $strict = false): string + { + if (!$encodedString || !\is_string($encodedString)) { + // Empty string, nothing to decode + return ''; + } + + $chars = $this->chars; + $bitsPerCharacter = $this->bitsPerCharacter; + $radix = $this->radix; + $rightPadFinalBits = $this->rightPadFinalBits; + $padCharacter = $this->padCharacter; + + // Get index of encoded characters + if ($this->charmap) { + $charmap = $this->charmap; + } else { + $charmap = []; + + for ($i = 0; $i < $radix; ++$i) { + $charmap[$chars[$i]] = $i; + } + + $this->charmap = $charmap; + } + + // The last encoded character is $encodedString[$lastNotatedIndex] + $lastNotatedIndex = \strlen($encodedString) - 1; + + // Remove trailing padding characters + while ($encodedString[$lastNotatedIndex] === $padCharacter[0]) { + $encodedString = substr($encodedString, 0, $lastNotatedIndex); + --$lastNotatedIndex; + } + + $rawString = ''; + $byte = 0; + $bitsWritten = 0; + + // Convert each encoded character to a series of unencoded bits + for ($c = 0; $c <= $lastNotatedIndex; ++$c) { + if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) { + // Encoded character was not found; try other case + if (isset($charmap[$cUpper = strtoupper($encodedString[$c])])) { + $charmap[$encodedString[$c]] = $charmap[$cUpper]; + } elseif (isset($charmap[$cLower = strtolower($encodedString[$c])])) { + $charmap[$encodedString[$c]] = $charmap[$cLower]; + } + } + + if (isset($charmap[$encodedString[$c]])) { + $bitsNeeded = 8 - $bitsWritten; + $unusedBitCount = $bitsPerCharacter - $bitsNeeded; + + // Get the new bits ready + if ($bitsNeeded > $bitsPerCharacter) { + // New bits aren't enough to complete a byte; shift them + // left into position + $newBits = $charmap[$encodedString[$c]] << $bitsNeeded + - $bitsPerCharacter; + $bitsWritten += $bitsPerCharacter; + } elseif ($c !== $lastNotatedIndex || $rightPadFinalBits) { + // Zero or more too many bits to complete a byte; + // shift right + $newBits = $charmap[$encodedString[$c]] >> $unusedBitCount; + $bitsWritten = 8; //$bitsWritten += $bitsNeeded; + } else { + // Final bits don't need to be shifted + $newBits = $charmap[$encodedString[$c]]; + $bitsWritten = 8; + } + + $byte |= $newBits; + + if (8 === $bitsWritten || $c === $lastNotatedIndex) { + // Byte is ready to be written + $rawString .= pack('C', $byte); + + if ($c !== $lastNotatedIndex) { + // Start the next byte + $bitsWritten = $unusedBitCount; + $byte = ($charmap[$encodedString[$c]] + ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten; + } + } + } elseif ($strict) { + // Unable to decode character; abort + return null; + } + } + + return $rawString; + } +} + +// NEXT_MAJOR: Remove class alias +class_alias('Sonata\GoogleAuthenticator\FixedBitNotation', 'Google\Authenticator\FixedBitNotation', false); diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php new file mode 100644 index 0000000..d3fc52f --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\GoogleAuthenticator; + +/** + * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + */ +final class GoogleAuthenticator implements GoogleAuthenticatorInterface +{ + /** + * @var int + */ + private $passCodeLength; + + /** + * @var int + */ + private $secretLength; + + /** + * @var int + */ + private $pinModulo; + + /** + * @var \DateTimeInterface + */ + private $instanceTime; + + /** + * @var int + */ + private $codePeriod; + + /** + * @var int + */ + private $periodSize = 30; + + public function __construct(int $passCodeLength = 6, int $secretLength = 10, ?\DateTimeInterface $instanceTime = null, int $codePeriod = 30) + { + /* + * codePeriod is the duration in seconds that the code is valid. + * periodSize is the length of a period to calculate periods since Unix epoch. + * periodSize cannot be larger than the codePeriod. + */ + + $this->passCodeLength = $passCodeLength; + $this->secretLength = $secretLength; + $this->codePeriod = $codePeriod; + $this->periodSize = $codePeriod < $this->periodSize ? $codePeriod : $this->periodSize; + $this->pinModulo = 10 ** $passCodeLength; + $this->instanceTime = $instanceTime ?? new \DateTimeImmutable(); + } + + /** + * @param string $secret + * @param string $code + * @param int $discrepancy + */ + public function checkCode($secret, $code, $discrepancy = 1): bool + { + /** + * Discrepancy is the factor of periodSize ($discrepancy * $periodSize) allowed on either side of the + * given codePeriod. For example, if a code with codePeriod = 60 is generated at 10:00:00, a discrepancy + * of 1 will allow a periodSize of 30 seconds on either side of the codePeriod resulting in a valid code + * from 09:59:30 to 10:00:29. + * + * The result of each comparison is stored as a timestamp here instead of using a guard clause + * (https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html). This is to implement + * constant time comparison to make side-channel attacks harder. See + * https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time for details. + * Each comparison uses hash_equals() instead of an operator to implement constant time equality comparison + * for each code. + */ + $periods = floor($this->codePeriod / $this->periodSize); + + $result = 0; + for ($i = -$discrepancy; $i < $periods + $discrepancy; ++$i) { + $dateTime = new \DateTimeImmutable('@'.($this->instanceTime->getTimestamp() - ($i * $this->periodSize))); + $result = hash_equals($this->getCode($secret, $dateTime), $code) ? $dateTime->getTimestamp() : $result; + } + + return $result > 0; + } + + /** + * NEXT_MAJOR: add the interface typehint to $time and remove deprecation. + * + * @param string $secret + * @param float|string|int|\DateTimeInterface|null $time + */ + public function getCode($secret, /* \DateTimeInterface */ $time = null): string + { + if (null === $time) { + $time = $this->instanceTime; + } + + if ($time instanceof \DateTimeInterface) { + $timeForCode = floor($time->getTimestamp() / $this->periodSize); + } else { + @trigger_error( + 'Passing anything other than null or a DateTimeInterface to $time is deprecated as of 2.0 '. + 'and will not be possible as of 3.0.', + \E_USER_DEPRECATED + ); + $timeForCode = $time; + } + + $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true); + $secret = $base32->decode($secret); + + $timeForCode = str_pad(pack('N', $timeForCode), 8, \chr(0), \STR_PAD_LEFT); + + $hash = hash_hmac('sha1', $timeForCode, $secret, true); + $offset = \ord(substr($hash, -1)); + $offset &= 0xF; + + $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF; + + return str_pad((string) ($truncatedHash % $this->pinModulo), $this->passCodeLength, '0', \STR_PAD_LEFT); + } + + /** + * NEXT_MAJOR: Remove this method. + * + * @param string $user + * @param string $hostname + * @param string $secret + * + * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead. + */ + public function getUrl($user, $hostname, $secret): string + { + @trigger_error(sprintf( + 'Using %s() is deprecated as of 2.1 and will be removed in 3.0. '. + 'Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.', + __METHOD__ + ), \E_USER_DEPRECATED); + + $issuer = \func_get_args()[3] ?? null; + $accountName = sprintf('%s@%s', $user, $hostname); + + // manually concat the issuer to avoid a change in URL + $url = GoogleQrUrl::generate($accountName, $secret); + + if ($issuer) { + $url .= '%26issuer%3D'.$issuer; + } + + return $url; + } + + public function generateSecret(): string + { + return (new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true)) + ->encode(random_bytes($this->secretLength)); + } + + private function hashToInt(string $bytes, int $start): int + { + return unpack('N', substr(substr($bytes, $start), 0, 4))[1]; + } +} + +// NEXT_MAJOR: Remove class alias +class_alias('Sonata\GoogleAuthenticator\GoogleAuthenticator', 'Google\Authenticator\GoogleAuthenticator', false); diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php new file mode 100644 index 0000000..4dbe601 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\GoogleAuthenticator; + +interface GoogleAuthenticatorInterface +{ + /** + * @param string $secret + * @param string $code + */ + public function checkCode($secret, $code, $discrepancy = 1): bool; + + /** + * NEXT_MAJOR: add the interface typehint to $time and remove deprecation. + * + * @param string $secret + * @param float|string|int|\DateTimeInterface|null $time + */ + public function getCode($secret, /* \DateTimeInterface */ $time = null): string; + + /** + * NEXT_MAJOR: Remove this method. + * + * @param string $user + * @param string $hostname + * @param string $secret + * + * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead. + */ + public function getUrl($user, $hostname, $secret): string; + + public function generateSecret(): string; +} diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php new file mode 100644 index 0000000..e0f6e60 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\GoogleAuthenticator; + +/** + * Responsible for QR image url generation. + * + * @see http://goqr.me/api/ + * @see http://goqr.me/api/doc/ + * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + * + * @author Iltar van der Berg+ * gd_version src_name src_name_body src_name_ext + * src_pathname src_mime src_x src_y + * src_type src_bits src_pixels + * src_size src_size_kb src_size_mb src_size_human + * dst_path dst_name_body dst_pathname + * dst_name dst_name_ext dst_x dst_y + * date time host server ip + *+ * The tokens must be enclosed in square brackets: [dst_x] will be replaced by the width of the picture + * + * Default value is null + * + * @access public + * @var string; + */ + var $image_text; + + /** + * Sets the text direction for the text label + * + * Value is either 'h' or 'v', as in horizontal and vertical + * + * Note that if you use a TrueType font, you can use {@link image_text_angle} instead + * + * Default value is h (horizontal) + * + * @access public + * @var string; + */ + var $image_text_direction; + + /** + * Sets the text color for the text label + * + * Value is an hexadecimal color, such as #FFFFFF + * + * Default value is #FFFFFF (white) + * + * @access public + * @var string; + */ + var $image_text_color; + + /** + * Sets the text opacity in the text label + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_text_opacity; + + /** + * Sets the text background color for the text label + * + * Value is an hexadecimal color, such as #FFFFFF + * + * Default value is null (no background) + * + * @access public + * @var string; + */ + var $image_text_background; + + /** + * Sets the text background opacity in the text label + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_text_background_opacity; + + /** + * Sets the text font in the text label + * + * Value is a an integer between 1 and 5 for GD built-in fonts. 1 is the smallest font, 5 the biggest + * Value can also be a string, which represents the path to a GDF or TTF font (TrueType). + * + * Default value is 5 + * + * @access public + * @var mixed; + */ + var $image_text_font; + + /** + * Sets the text font size for TrueType fonts + * + * Value is a an integer, and represents the font size in pixels (GD1) or points (GD1) + * + * Note that this setting is only applicable to TrueType fonts, and has no effects with GD fonts + * + * Default value is 16 + * + * @access public + * @var integer; + */ + var $image_text_size; + + /** + * Sets the text angle for TrueType fonts + * + * Value is a an integer between 0 and 360, in degrees, with 0 degrees being left-to-right reading text. + * + * Note that this setting is only applicable to TrueType fonts, and has no effects with GD fonts + * For GD fonts, you can use {@link image_text_direction} instead + * + * Default value is null (so it is determined by the value of {@link image_text_direction}) + * + * @access public + * @var integer; + */ + var $image_text_angle; + + /** + * Sets the text label position within the image + * + * Value is one or two out of 'TBLR' (top, bottom, left, right) + * + * The positions are as following: + *
+ * TL T TR + * L R + * BL B BR + *+ * + * Default value is null (centered, horizontal and vertical) + * + * Note that is {@link image_text_x} and {@link image_text_y} are used, this setting has no effect + * + * @access public + * @var string; + */ + var $image_text_position; + + /** + * Sets the text label absolute X position within the image + * + * Value is in pixels, representing the distance between the left of the image and the label + * If a negative value is used, it will represent the distance between the right of the image and the label + * + * Default value is null (so {@link image_text_position} is used) + * + * @access public + * @var integer + */ + var $image_text_x; + + /** + * Sets the text label absolute Y position within the image + * + * Value is in pixels, representing the distance between the top of the image and the label + * If a negative value is used, it will represent the distance between the bottom of the image and the label + * + * Default value is null (so {@link image_text_position} is used) + * + * @access public + * @var integer + */ + var $image_text_y; + + /** + * Sets the text label padding + * + * Value is in pixels, representing the distance between the text and the label background border + * + * Default value is 0 + * + * This setting can be overriden by {@link image_text_padding_x} and {@link image_text_padding_y} + * + * @access public + * @var integer + */ + var $image_text_padding; + + /** + * Sets the text label horizontal padding + * + * Value is in pixels, representing the distance between the text and the left and right label background borders + * + * Default value is null + * + * If set, this setting overrides the horizontal part of {@link image_text_padding} + * + * @access public + * @var integer + */ + var $image_text_padding_x; + + /** + * Sets the text label vertical padding + * + * Value is in pixels, representing the distance between the text and the top and bottom label background borders + * + * Default value is null + * + * If set, his setting overrides the vertical part of {@link image_text_padding} + * + * @access public + * @var integer + */ + var $image_text_padding_y; + + /** + * Sets the text alignment + * + * Value is a string, which can be either 'L', 'C' or 'R' + * + * Default value is 'C' + * + * This setting is relevant only if the text has several lines. + * + * Note that this setting is only applicable to GD fonts, and has no effects with TrueType fonts + * + * @access public + * @var string; + */ + var $image_text_alignment; + + /** + * Sets the text line spacing + * + * Value is an integer, in pixels + * + * Default value is 0 + * + * This setting is relevant only if the text has several lines. + * + * Note that this setting is only applicable to GD fonts, and has no effects with TrueType fonts + * + * @access public + * @var integer + */ + var $image_text_line_spacing; + + /** + * Sets the height of the reflection + * + * Value is an integer in pixels, or a string which format can be in pixels or percentage. + * For instance, values can be : 40, '40', '40px' or '40%' + * + * Default value is null, no reflection + * + * @access public + * @var mixed; + */ + var $image_reflection_height; + + /** + * Sets the space between the source image and its relection + * + * Value is an integer in pixels, which can be negative + * + * Default value is 2 + * + * This setting is relevant only if {@link image_reflection_height} is set + * + * @access public + * @var integer + */ + var $image_reflection_space; + + /** + * Sets the initial opacity of the reflection + * + * Value is an integer between 0 (no opacity) and 100 (full opacity). + * The reflection will start from {@link image_reflection_opacity} and end up at 0 + * + * Default value is 60 + * + * This setting is relevant only if {@link image_reflection_height} is set + * + * @access public + * @var integer + */ + var $image_reflection_opacity; + + /** + * Automatically rotates the image according to EXIF data (JPEG only) + * + * Default value is true + * + * @access public + * @var boolean; + */ + var $image_auto_rotate; + + /** + * Flips the image vertically or horizontally + * + * Value is either 'h' or 'v', as in horizontal and vertical + * + * Default value is null (no flip) + * + * @access public + * @var string; + */ + var $image_flip; + + /** + * Rotates the image by increments of 45 degrees + * + * Value is either 90, 180 or 270 + * + * Default value is null (no rotation) + * + * @access public + * @var string; + */ + var $image_rotate; + + /** + * Crops an image + * + * Values are four dimensions, or two, or one (CSS style) + * They represent the amount cropped top, right, bottom and left. + * These values can either be in an array, or a space separated string. + * Each value can be in pixels (with or without 'px'), or percentage (of the source image) + * + * For instance, are valid: + *
+ * $foo->image_crop = 20 OR array(20);
+ * $foo->image_crop = '20px' OR array('20px');
+ * $foo->image_crop = '20 40' OR array('20', 40);
+ * $foo->image_crop = '-20 25%' OR array(-20, '25%');
+ * $foo->image_crop = '20px 25%' OR array('20px', '25%');
+ * $foo->image_crop = '20% 25%' OR array('20%', '25%');
+ * $foo->image_crop = '20% 25% 10% 30%' OR array('20%', '25%', '10%', '30%');
+ * $foo->image_crop = '20px 25px 2px 2px' OR array('20px', '25%px', '2px', '2px');
+ * $foo->image_crop = '20 25% 40px 10%' OR array(20, '25%', '40px', '10%');
+ *
+ *
+ * If a value is negative, the image will be expanded, and the extra parts will be filled with black
+ *
+ * Default value is null (no cropping)
+ *
+ * @access public
+ * @var string OR array;
+ */
+ var $image_crop;
+
+ /**
+ * Crops an image, before an eventual resizing
+ *
+ * See {@link image_crop} for valid formats
+ *
+ * Default value is null (no cropping)
+ *
+ * @access public
+ * @var string OR array;
+ */
+ var $image_precrop;
+
+ /**
+ * Adds a bevel border on the image
+ *
+ * Value is a positive integer, representing the thickness of the bevel
+ *
+ * If the bevel colors are the same as the background, it makes a fade out effect
+ *
+ * Default value is null (no bevel)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_bevel;
+
+ /**
+ * Top and left bevel color
+ *
+ * Value is a color, in hexadecimal format
+ * This setting is used only if {@link image_bevel} is set
+ *
+ * Default value is #FFFFFF
+ *
+ * @access public
+ * @var string;
+ */
+ var $image_bevel_color1;
+
+ /**
+ * Right and bottom bevel color
+ *
+ * Value is a color, in hexadecimal format
+ * This setting is used only if {@link image_bevel} is set
+ *
+ * Default value is #000000
+ *
+ * @access public
+ * @var string;
+ */
+ var $image_bevel_color2;
+
+ /**
+ * Adds a single-color border on the outer of the image
+ *
+ * Values are four dimensions, or two, or one (CSS style)
+ * They represent the border thickness top, right, bottom and left.
+ * These values can either be in an array, or a space separated string.
+ * Each value can be in pixels (with or without 'px'), or percentage (of the source image)
+ *
+ * See {@link image_crop} for valid formats
+ *
+ * If a value is negative, the image will be cropped.
+ * Note that the dimensions of the picture will be increased by the borders' thickness
+ *
+ * Default value is null (no border)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_border;
+
+ /**
+ * Border color
+ *
+ * Value is a color, in hexadecimal format.
+ * This setting is used only if {@link image_border} is set
+ *
+ * Default value is #FFFFFF
+ *
+ * @access public
+ * @var string;
+ */
+ var $image_border_color;
+
+ /**
+ * Sets the opacity for the borders
+ *
+ * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque)
+ *
+ * Unless used with {@link image_border}, this setting has no effect
+ *
+ * Default value is 100
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_border_opacity;
+
+ /**
+ * Adds a fading-to-transparent border on the image
+ *
+ * Values are four dimensions, or two, or one (CSS style)
+ * They represent the border thickness top, right, bottom and left.
+ * These values can either be in an array, or a space separated string.
+ * Each value can be in pixels (with or without 'px'), or percentage (of the source image)
+ *
+ * See {@link image_crop} for valid formats
+ *
+ * Note that the dimensions of the picture will not be increased by the borders' thickness
+ *
+ * Default value is null (no border)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_border_transparent;
+
+ /**
+ * Adds a multi-color frame on the outer of the image
+ *
+ * Value is an integer. Two values are possible for now:
+ * 1 for flat border, meaning that the frame is mirrored horizontally and vertically
+ * 2 for crossed border, meaning that the frame will be inversed, as in a bevel effect
+ *
+ * The frame will be composed of colored lines set in {@link image_frame_colors}
+ *
+ * Note that the dimensions of the picture will be increased by the borders' thickness
+ *
+ * Default value is null (no frame)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_frame;
+
+ /**
+ * Sets the colors used to draw a frame
+ *
+ * Values is a list of n colors in hexadecimal format.
+ * These values can either be in an array, or a space separated string.
+ *
+ * The colors are listed in the following order: from the outset of the image to its center
+ *
+ * For instance, are valid:
+ *
+ * $foo->image_frame_colors = '#FFFFFF #999999 #666666 #000000';
+ * $foo->image_frame_colors = array('#FFFFFF', '#999999', '#666666', '#000000');
+ *
+ *
+ * This setting is used only if {@link image_frame} is set
+ *
+ * Default value is '#FFFFFF #999999 #666666 #000000'
+ *
+ * @access public
+ * @var string OR array;
+ */
+ var $image_frame_colors;
+
+ /**
+ * Sets the opacity for the frame
+ *
+ * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque)
+ *
+ * Unless used with {@link image_frame}, this setting has no effect
+ *
+ * Default value is 100
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_frame_opacity;
+
+ /**
+ * Adds a watermark on the image
+ *
+ * Value is a local image filename, relative or absolute. GIF, JPG, BMP, WEBP and PNG are supported, as well as PNG and WEBP alpha.
+ *
+ * If set, this setting allow the use of all other settings starting with image_watermark_
+ *
+ * Default value is null
+ *
+ * @access public
+ * @var string;
+ */
+ var $image_watermark;
+
+ /**
+ * Sets the watermarkposition within the image
+ *
+ * Value is one or two out of 'TBLR' (top, bottom, left, right)
+ *
+ * The positions are as following: TL T TR
+ * L R
+ * BL B BR
+ *
+ * Default value is null (centered, horizontal and vertical)
+ *
+ * Note that is {@link image_watermark_x} and {@link image_watermark_y} are used, this setting has no effect
+ *
+ * @access public
+ * @var string;
+ */
+ var $image_watermark_position;
+
+ /**
+ * Sets the watermark absolute X position within the image
+ *
+ * Value is in pixels, representing the distance between the top of the image and the watermark
+ * If a negative value is used, it will represent the distance between the bottom of the image and the watermark
+ *
+ * Default value is null (so {@link image_watermark_position} is used)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_watermark_x;
+
+ /**
+ * Sets the twatermark absolute Y position within the image
+ *
+ * Value is in pixels, representing the distance between the left of the image and the watermark
+ * If a negative value is used, it will represent the distance between the right of the image and the watermark
+ *
+ * Default value is null (so {@link image_watermark_position} is used)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_watermark_y;
+
+ /**
+ * Prevents the watermark to be resized up if it is smaller than the image
+ *
+ * If the watermark if smaller than the destination image, taking in account the desired watermark position
+ * then it will be resized up to fill in the image (minus the {@link image_watermark_x} or {@link image_watermark_y} values)
+ *
+ * If you don't want your watermark to be resized in any way, then
+ * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to true
+ * If you want your watermark to be resized up or doan to fill in the image better, then
+ * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to false
+ *
+ * Default value is true (so the watermark will not be resized up, which is the behaviour most people expect)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_watermark_no_zoom_in;
+
+ /**
+ * Prevents the watermark to be resized down if it is bigger than the image
+ *
+ * If the watermark if bigger than the destination image, taking in account the desired watermark position
+ * then it will be resized down to fit in the image (minus the {@link image_watermark_x} or {@link image_watermark_y} values)
+ *
+ * If you don't want your watermark to be resized in any way, then
+ * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to true
+ * If you want your watermark to be resized up or doan to fill in the image better, then
+ * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to false
+ *
+ * Default value is false (so the watermark may be shrinked to fit in the image)
+ *
+ * @access public
+ * @var integer
+ */
+ var $image_watermark_no_zoom_out;
+
+ /**
+ * List of MIME types per extension
+ *
+ * @access private
+ * @var array
+ */
+ var $mime_types;
+
+ /**
+ * Allowed MIME types
+ *
+ * Default is a selection of safe mime-types, but you might want to change it
+ *
+ * Simple wildcards are allowed, such as image/* or application/*
+ * If there is only one MIME type allowed, then it can be a string instead of an array
+ *
+ * @access public
+ * @var array OR string
+ */
+ var $allowed;
+
+ /**
+ * Forbidden MIME types
+ *
+ * Default is a selection of safe mime-types, but you might want to change it
+ * To only check for forbidden MIME types, and allow everything else, set {@link allowed} to array('* / *') without the spaces
+ *
+ * Simple wildcards are allowed, such as image/* or application/*
+ * If there is only one MIME type forbidden, then it can be a string instead of an array
+ *
+ * @access public
+ * @var array OR string
+ */
+ var $forbidden;
+
+ /**
+ * Blacklisted file extensions
+ *
+ * List of blacklisted extensions, that are enforced if {@link no_script} is true
+ *
+ * @access public
+ * @var array
+ */
+ var $blacklist;
+
+
+ /**
+ * Array of translated error messages
+ *
+ * By default, the language is english (en_GB)
+ * Translations can be in separate files, in a lang/ subdirectory
+ *
+ * @access public
+ * @var array
+ */
+ var $translation;
+
+ /**
+ * Language selected for the translations
+ *
+ * By default, the language is english ("en_GB")
+ *
+ * @access public
+ * @var array
+ */
+ var $lang;
+
+ /**
+ * Init or re-init all the processing variables to their default values
+ *
+ * This function is called in the constructor, and after each call of {@link process}
+ *
+ * @access private
+ */
+ function init() {
+
+ // overiddable variables
+ $this->file_new_name_body = null; // replace the name body
+ $this->file_name_body_add = null; // append to the name body
+ $this->file_name_body_pre = null; // prepend to the name body
+ $this->file_new_name_ext = null; // replace the file extension
+ $this->file_safe_name = true; // format safely the filename
+ $this->file_force_extension = true; // forces extension if there isn't one
+ $this->file_overwrite = false; // allows overwritting if the file already exists
+ $this->file_auto_rename = true; // auto-rename if the file already exists
+ $this->dir_auto_create = true; // auto-creates directory if missing
+ $this->dir_auto_chmod = true; // auto-chmod directory if not writeable
+ $this->dir_chmod = 0755; // default chmod to use
+
+ $this->no_script = true; // turns scripts into test files
+ $this->mime_check = true; // checks the mime type against the allowed list
+
+ // these are the different MIME detection methods. if one of these method doesn't work on your
+ // system, you can deactivate it here; just set it to false
+ $this->mime_fileinfo = true; // MIME detection with Fileinfo PECL extension
+ $this->mime_file = true; // MIME detection with UNIX file() command
+ $this->mime_magic = true; // MIME detection with mime_magic (mime_content_type())
+ $this->mime_getimagesize = true; // MIME detection with getimagesize()
+
+ // get the default max size from php.ini
+ $this->file_max_size_raw = trim(ini_get('upload_max_filesize'));
+ $this->file_max_size = $this->getsize($this->file_max_size_raw);
+
+ $this->image_resize = false; // resize the image
+ $this->image_convert = ''; // convert. values :''; 'png'; 'jpeg'; 'gif'; 'bmp'; 'webp'
+
+ $this->image_x = 150;
+ $this->image_y = 150;
+ $this->image_ratio = false; // keeps aspect ratio within x and y dimensions
+ $this->image_ratio_crop = false; // keeps aspect ratio within x and y dimensions, filling the space
+ $this->image_ratio_fill = false; // keeps aspect ratio within x and y dimensions, fitting the image in the space
+ $this->image_ratio_pixels = false; // keeps aspect ratio, calculating x and y to reach the number of pixels
+ $this->image_ratio_x = false; // calculate the $image_x if true
+ $this->image_ratio_y = false; // calculate the $image_y if true
+ $this->image_ratio_no_zoom_in = false;
+ $this->image_ratio_no_zoom_out = false;
+ $this->image_no_enlarging = false;
+ $this->image_no_shrinking = false;
+
+ $this->png_compression = null;
+ $this->webp_quality = 85;
+ $this->jpeg_quality = 85;
+ $this->jpeg_size = null;
+ $this->image_interlace = false;
+ $this->image_is_transparent = false;
+ $this->image_transparent_color = null;
+ $this->image_background_color = null;
+ $this->image_default_color = '#ffffff';
+ $this->image_is_palette = false;
+
+ $this->image_max_width = null;
+ $this->image_max_height = null;
+ $this->image_max_pixels = null;
+ $this->image_max_ratio = null;
+ $this->image_min_width = null;
+ $this->image_min_height = null;
+ $this->image_min_pixels = null;
+ $this->image_min_ratio = null;
+
+ $this->image_brightness = null;
+ $this->image_contrast = null;
+ $this->image_opacity = null;
+ $this->image_threshold = null;
+ $this->image_tint_color = null;
+ $this->image_overlay_color = null;
+ $this->image_overlay_opacity = null;
+ $this->image_negative = false;
+ $this->image_greyscale = false;
+ $this->image_pixelate = null;
+ $this->image_unsharp = false;
+ $this->image_unsharp_amount = 80;
+ $this->image_unsharp_radius = 0.5;
+ $this->image_unsharp_threshold = 1;
+
+ $this->image_text = null;
+ $this->image_text_direction = null;
+ $this->image_text_color = '#FFFFFF';
+ $this->image_text_opacity = 100;
+ $this->image_text_background = null;
+ $this->image_text_background_opacity = 100;
+ $this->image_text_font = 5;
+ $this->image_text_size = 16;
+ $this->image_text_angle = null;
+ $this->image_text_x = null;
+ $this->image_text_y = null;
+ $this->image_text_position = null;
+ $this->image_text_padding = 0;
+ $this->image_text_padding_x = null;
+ $this->image_text_padding_y = null;
+ $this->image_text_alignment = 'C';
+ $this->image_text_line_spacing = 0;
+
+ $this->image_reflection_height = null;
+ $this->image_reflection_space = 2;
+ $this->image_reflection_opacity = 60;
+
+ $this->image_watermark = null;
+ $this->image_watermark_x = null;
+ $this->image_watermark_y = null;
+ $this->image_watermark_position = null;
+ $this->image_watermark_no_zoom_in = true;
+ $this->image_watermark_no_zoom_out = false;
+
+ $this->image_flip = null;
+ $this->image_auto_rotate = true;
+ $this->image_rotate = null;
+ $this->image_crop = null;
+ $this->image_precrop = null;
+
+ $this->image_bevel = null;
+ $this->image_bevel_color1 = '#FFFFFF';
+ $this->image_bevel_color2 = '#000000';
+ $this->image_border = null;
+ $this->image_border_color = '#FFFFFF';
+ $this->image_border_opacity = 100;
+ $this->image_border_transparent = null;
+ $this->image_frame = null;
+ $this->image_frame_colors = '#FFFFFF #999999 #666666 #000000';
+ $this->image_frame_opacity = 100;
+
+ $this->forbidden = array();
+ $this->allowed = array(
+ 'application/arj',
+ 'application/excel',
+ 'application/gnutar',
+ 'application/mspowerpoint',
+ 'application/msword',
+ 'application/octet-stream',
+ 'application/onenote',
+ 'application/pdf',
+ 'application/plain',
+ 'application/postscript',
+ 'application/powerpoint',
+ 'application/rar',
+ 'application/rtf',
+ 'application/vnd.ms-excel',
+ 'application/vnd.ms-excel.addin.macroEnabled.12',
+ 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+ 'application/vnd.ms-excel.sheet.macroEnabled.12',
+ 'application/vnd.ms-excel.template.macroEnabled.12',
+ 'application/vnd.ms-office',
+ 'application/vnd.ms-officetheme',
+ 'application/vnd.ms-powerpoint',
+ 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
+ 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+ 'application/vnd.ms-powerpoint.slide.macroEnabled.12',
+ 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+ 'application/vnd.ms-powerpoint.template.macroEnabled.12',
+ 'application/vnd.ms-word',
+ 'application/vnd.ms-word.document.macroEnabled.12',
+ 'application/vnd.ms-word.template.macroEnabled.12',
+ 'application/vnd.oasis.opendocument.chart',
+ 'application/vnd.oasis.opendocument.database',
+ 'application/vnd.oasis.opendocument.formula',
+ 'application/vnd.oasis.opendocument.graphics',
+ 'application/vnd.oasis.opendocument.graphics-template',
+ 'application/vnd.oasis.opendocument.image',
+ 'application/vnd.oasis.opendocument.presentation',
+ 'application/vnd.oasis.opendocument.presentation-template',
+ 'application/vnd.oasis.opendocument.spreadsheet',
+ 'application/vnd.oasis.opendocument.spreadsheet-template',
+ 'application/vnd.oasis.opendocument.text',
+ 'application/vnd.oasis.opendocument.text-master',
+ 'application/vnd.oasis.opendocument.text-template',
+ 'application/vnd.oasis.opendocument.text-web',
+ 'application/vnd.openofficeorg.extension',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+ 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+ 'application/vnd.openxmlformats-officedocument.presentationml.template',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+ 'application/vocaltec-media-file',
+ 'application/wordperfect',
+ 'application/haansoftxlsx',
+ 'application/x-bittorrent',
+ 'application/x-bzip',
+ 'application/x-bzip2',
+ 'application/x-compressed',
+ 'application/x-excel',
+ 'application/x-gzip',
+ 'application/x-latex',
+ 'application/x-midi',
+ 'application/xml',
+ 'application/x-msexcel',
+ 'application/x-rar',
+ 'application/x-rar-compressed',
+ 'application/x-rtf',
+ 'application/x-shockwave-flash',
+ 'application/x-sit',
+ 'application/x-stuffit',
+ 'application/x-troff-msvideo',
+ 'application/x-zip',
+ 'application/x-zip-compressed',
+ 'application/zip',
+ 'audio/*',
+ 'image/*',
+ 'multipart/x-gzip',
+ 'multipart/x-zip',
+ 'text/plain',
+ 'text/rtf',
+ 'text/richtext',
+ 'text/xml',
+ 'video/*',
+ 'text/csv',
+ 'text/x-c',
+ 'text/x-csv',
+ 'text/comma-separated-values',
+ 'text/x-comma-separated-values',
+ 'application/csv',
+ 'application/x-csv',
+ );
+
+ $this->mime_types = array(
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'jpe' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'webp' => 'image/webp',
+ 'png' => 'image/png',
+ 'bmp' => 'image/bmp',
+ 'flif' => 'image/flif',
+ 'flv' => 'video/x-flv',
+ 'js' => 'application/x-javascript',
+ 'json' => 'application/json',
+ 'tiff' => 'image/tiff',
+ 'css' => 'text/css',
+ 'xml' => 'application/xml',
+ 'doc' => 'application/msword',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlt' => 'application/vnd.ms-excel',
+ 'xlm' => 'application/vnd.ms-excel',
+ 'xld' => 'application/vnd.ms-excel',
+ 'xla' => 'application/vnd.ms-excel',
+ 'xlc' => 'application/vnd.ms-excel',
+ 'xlw' => 'application/vnd.ms-excel',
+ 'xll' => 'application/vnd.ms-excel',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pps' => 'application/vnd.ms-powerpoint',
+ 'rtf' => 'application/rtf',
+ 'pdf' => 'application/pdf',
+ 'html' => 'text/html',
+ 'htm' => 'text/html',
+ 'php' => 'text/html',
+ 'txt' => 'text/plain',
+ 'mpeg' => 'video/mpeg',
+ 'mpg' => 'video/mpeg',
+ 'mpe' => 'video/mpeg',
+ 'mp3' => 'audio/mpeg3',
+ 'wav' => 'audio/wav',
+ 'aiff' => 'audio/aiff',
+ 'aif' => 'audio/aiff',
+ 'avi' => 'video/msvideo',
+ 'wmv' => 'video/x-ms-wmv',
+ 'mov' => 'video/quicktime',
+ 'zip' => 'application/zip',
+ 'tar' => 'application/x-tar',
+ 'swf' => 'application/x-shockwave-flash',
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'ott' => 'application/vnd.oasis.opendocument.text-template',
+ 'oth' => 'application/vnd.oasis.opendocument.text-web',
+ 'odm' => 'application/vnd.oasis.opendocument.text-master',
+ 'odg' => 'application/vnd.oasis.opendocument.graphics',
+ 'otg' => 'application/vnd.oasis.opendocument.graphics-template',
+ 'odp' => 'application/vnd.oasis.opendocument.presentation',
+ 'otp' => 'application/vnd.oasis.opendocument.presentation-template',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
+ 'odc' => 'application/vnd.oasis.opendocument.chart',
+ 'odf' => 'application/vnd.oasis.opendocument.formula',
+ 'odb' => 'application/vnd.oasis.opendocument.database',
+ 'odi' => 'application/vnd.oasis.opendocument.image',
+ 'oxt' => 'application/vnd.openofficeorg.extension',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
+ 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+ 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+ 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+ 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
+ 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+ 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+ 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+ 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+ 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+ 'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
+ 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
+ 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+ 'sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12',
+ 'thmx' => 'application/vnd.ms-officetheme',
+ 'onetoc' => 'application/onenote',
+ 'onetoc2' => 'application/onenote',
+ 'onetmp' => 'application/onenote',
+ 'onepkg' => 'application/onenote',
+ 'csv' => 'text/csv',
+ );
+
+ $this->blacklist = array(
+ 'php',
+ 'php7',
+ 'php6',
+ 'php5',
+ 'php4',
+ 'php3',
+ 'phtml',
+ 'pht',
+ 'phpt',
+ 'phtm',
+ 'phps',
+ 'inc',
+ 'pl',
+ 'py',
+ 'cgi',
+ 'asp',
+ 'js',
+ 'sh',
+ 'phar',
+ );
+
+ }
+
+ /**
+ * Constructor, for PHP5+
+ */
+ function __construct($file, $lang = 'en_GB') {
+ $this->upload($file, $lang);
+ }
+
+ /**
+ * Constructor, for PHP4. Checks if the file has been uploaded
+ *
+ * The constructor takes $_FILES['form_field'] array as argument
+ * where form_field is the form field name
+ *
+ * The constructor will check if the file has been uploaded in its temporary location, and
+ * accordingly will set {@link uploaded} (and {@link error} is an error occurred)
+ *
+ * If the file has been uploaded, the constructor will populate all the variables holding the upload
+ * information (none of the processing class variables are used here).
+ * You can have access to information about the file (name, size, MIME type...).
+ *
+ *
+ * Alternatively, you can set the first argument to be a local filename (string)
+ * This allows processing of a local file, as if the file was uploaded
+ *
+ * The optional second argument allows you to set the language for the error messages
+ *
+ * @access private
+ * @param array $file $_FILES['form_field']
+ * or string $file Local filename
+ * @param string $lang Optional language code
+ */
+ function upload($file, $lang = 'en_GB') {
+
+ $this->version = '05/10/2021';
+
+ $this->file_src_name = '';
+ $this->file_src_name_body = '';
+ $this->file_src_name_ext = '';
+ $this->file_src_mime = '';
+ $this->file_src_size = '';
+ $this->file_src_error = '';
+ $this->file_src_pathname = '';
+ $this->file_src_temp = '';
+
+ $this->file_dst_path = '';
+ $this->file_dst_name = '';
+ $this->file_dst_name_body = '';
+ $this->file_dst_name_ext = '';
+ $this->file_dst_pathname = '';
+
+ $this->image_src_x = null;
+ $this->image_src_y = null;
+ $this->image_src_bits = null;
+ $this->image_src_type = null;
+ $this->image_src_pixels = null;
+ $this->image_dst_x = 0;
+ $this->image_dst_y = 0;
+ $this->image_dst_type = '';
+
+ $this->uploaded = true;
+ $this->no_upload_check = false;
+ $this->processed = false;
+ $this->error = '';
+ $this->log = '';
+ $this->allowed = array();
+ $this->forbidden = array();
+ $this->file_is_image = false;
+ $this->init();
+ $info = null;
+ $mime_from_browser = null;
+
+ // sets default language
+ $this->translation = array();
+ $this->translation['file_error'] = 'File error. Please try again.';
+ $this->translation['local_file_missing'] = 'Local file doesn\'t exist.';
+ $this->translation['local_file_not_readable'] = 'Local file is not readable.';
+ $this->translation['uploaded_too_big_ini'] = 'File upload error (the uploaded file exceeds the upload_max_filesize directive in php.ini).';
+ $this->translation['uploaded_too_big_html'] = 'File upload error (the uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the html form).';
+ $this->translation['uploaded_partial'] = 'File upload error (the uploaded file was only partially uploaded).';
+ $this->translation['uploaded_missing'] = 'File upload error (no file was uploaded).';
+ $this->translation['uploaded_no_tmp_dir'] = 'File upload error (missing a temporary folder).';
+ $this->translation['uploaded_cant_write'] = 'File upload error (failed to write file to disk).';
+ $this->translation['uploaded_err_extension'] = 'File upload error (file upload stopped by extension).';
+ $this->translation['uploaded_unknown'] = 'File upload error (unknown error code).';
+ $this->translation['try_again'] = 'File upload error. Please try again.';
+ $this->translation['file_too_big'] = 'File too big.';
+ $this->translation['no_mime'] = 'MIME type can\'t be detected.';
+ $this->translation['incorrect_file'] = 'Incorrect type of file.';
+ $this->translation['image_too_wide'] = 'Image too wide.';
+ $this->translation['image_too_narrow'] = 'Image too narrow.';
+ $this->translation['image_too_high'] = 'Image too tall.';
+ $this->translation['image_too_short'] = 'Image too short.';
+ $this->translation['ratio_too_high'] = 'Image ratio too high (image too wide).';
+ $this->translation['ratio_too_low'] = 'Image ratio too low (image too high).';
+ $this->translation['too_many_pixels'] = 'Image has too many pixels.';
+ $this->translation['not_enough_pixels'] = 'Image has not enough pixels.';
+ $this->translation['file_not_uploaded'] = 'File not uploaded. Can\'t carry on a process.';
+ $this->translation['already_exists'] = '%s already exists. Please change the file name.';
+ $this->translation['temp_file_missing'] = 'No correct temp source file. Can\'t carry on a process.';
+ $this->translation['source_missing'] = 'No correct uploaded source file. Can\'t carry on a process.';
+ $this->translation['destination_dir'] = 'Destination directory can\'t be created. Can\'t carry on a process.';
+ $this->translation['destination_dir_missing'] = 'Destination directory doesn\'t exist. Can\'t carry on a process.';
+ $this->translation['destination_path_not_dir'] = 'Destination path is not a directory. Can\'t carry on a process.';
+ $this->translation['destination_dir_write'] = 'Destination directory can\'t be made writeable. Can\'t carry on a process.';
+ $this->translation['destination_path_write'] = 'Destination path is not a writeable. Can\'t carry on a process.';
+ $this->translation['temp_file'] = 'Can\'t create the temporary file. Can\'t carry on a process.';
+ $this->translation['source_not_readable'] = 'Source file is not readable. Can\'t carry on a process.';
+ $this->translation['no_create_support'] = 'No create from %s support.';
+ $this->translation['create_error'] = 'Error in creating %s image from source.';
+ $this->translation['source_invalid'] = 'Can\'t read image source. Not an image?.';
+ $this->translation['gd_missing'] = 'GD doesn\'t seem to be present.';
+ $this->translation['watermark_no_create_support'] = 'No create from %s support, can\'t read watermark.';
+ $this->translation['watermark_create_error'] = 'No %s read support, can\'t create watermark.';
+ $this->translation['watermark_invalid'] = 'Unknown image format, can\'t read watermark.';
+ $this->translation['file_create'] = 'No %s create support.';
+ $this->translation['no_conversion_type'] = 'No conversion type defined.';
+ $this->translation['copy_failed'] = 'Error copying file on the server. copy() failed.';
+ $this->translation['reading_failed'] = 'Error reading the file.';
+
+ // determines the language
+ $this->lang = $lang;
+ if ($this->lang != 'en_GB' && file_exists(dirname(__FILE__).'/lang') && file_exists(dirname(__FILE__).'/lang/class.upload.' . $lang . '.php')) {
+ $translation = null;
+ include(dirname(__FILE__).'/lang/class.upload.' . $lang . '.php');
+ if (is_array($translation)) {
+ $this->translation = array_merge($this->translation, $translation);
+ } else {
+ $this->lang = 'en_GB';
+ }
+ }
+
+
+ // determines the supported MIME types, and matching image format
+ $this->image_supported = array();
+ if ($this->gdversion()) {
+ if (imagetypes() & IMG_GIF) {
+ $this->image_supported['image/gif'] = 'gif';
+ }
+ if (imagetypes() & IMG_JPG) {
+ $this->image_supported['image/jpg'] = 'jpg';
+ $this->image_supported['image/jpeg'] = 'jpg';
+ $this->image_supported['image/pjpeg'] = 'jpg';
+ }
+ if (imagetypes() & IMG_PNG) {
+ $this->image_supported['image/png'] = 'png';
+ $this->image_supported['image/x-png'] = 'png';
+ }
+ if (imagetypes() & IMG_WEBP) {
+ $this->image_supported['image/webp'] = 'webp';
+ $this->image_supported['image/x-webp'] = 'webp';
+ }
+ if (imagetypes() & IMG_WBMP) {
+ $this->image_supported['image/bmp'] = 'bmp';
+ $this->image_supported['image/x-ms-bmp'] = 'bmp';
+ $this->image_supported['image/x-windows-bmp'] = 'bmp';
+ }
+ }
+
+ // display some system information
+ if (empty($this->log)) {
+ $this->log .= 'system information
+
+ | Avatar | +Name | +Blurb | +Location / Last Seen | +
|---|---|---|---|
| =$row->username?> | +=Polygon::FilterText($row->blurb)?> | +Attributes?>>=$Status->Text?> | +|
| + | Group | +Description | +Members | +
| =Polygon::FilterText($row->name)?> | +=Polygon::FilterText($row->description)?> | +=$row->MemberCount?> | +
No results matched your search query
+Pages > 1) { ?> + +buildFooter(); ?> diff --git a/catalog.php b/catalog.php new file mode 100644 index 0000000..2415237 --- /dev/null +++ b/catalog.php @@ -0,0 +1,249 @@ + ["name" => "All Categories", "price" => true], + 3 => ["name" => "Clothing", "subcategories" => [8, 11, 2, 12], "price" => true], + 4 => ["name" => "Body Parts", "subcategories" => [17, 18], "price" => true], + 5 => ["name" => "Gear", "type" => 19, "price" => true], + 6 => ["name" => "Models", "type" => 10, "price" => false], + 8 => ["name" => "Decals", "type" => 13, "price" => false], + 9 => ["name" => "Audio", "type" => 3, "price" => false], +]; + +$sorts = +[ + 0 => "sales DESC", + 1 => "updated DESC", + 2 => "price DESC", + 3 => "price" +]; + +function getUrl($strings) +{ + global $cat, $subcat; + $url = "?"; + if($subcat) $url .= "Subcategory=".$subcat."&"; + $url .= "CurrencyType=%currency%&SortType=%sort%&Category=".$cat; + + return str_replace(["%currency%", "%sort%"], $strings, $url); +} + +$cat = $_GET['Category'] ?? 3; +$subcat = $_GET['Subcategory'] ?? false; +$keyword = $_GET['Keyword'] ?? ""; +$currency = $_GET['CurrencyType'] ?? 0; +$sort = $_GET['SortType'] ?? 1; +$page = $_GET['PageNumber'] ?? 1; + +if($cat == 3 && !isset($_GET['Category'])) + $subcat = 8; + +if(!isset($cats[$cat])) + die(header("Location: /catalog")); + +if($subcat && ($cat == 1 || isset($cats[$cat]["type"]) || !is_numeric($subcat) || !in_array($subcat, $cats[$cat]["subcategories"]))) + die(header("Location: /catalog?Category=".$cat)); + +if(!in_array($currency, [0, 1, 2])) + die(header("Location: ".getUrl([0, $sort]))); + +if(!isset($sorts[$sort])) + die(header("Location: ".getUrl([$currency, 0]))); + +$queryparam = ""; +$type = $subcat ?: $cats[$cat]["type"] ?? 2; + +// adding "is not null" fetches the item even if the price is 0 +$unavailable = isset($_GET['IncludeNotForSale']) && $_GET['IncludeNotForSale'] == "true"; +if($unavailable) $queryparam .= "IS NOT NULL "; + +// process query parameters for the item type +$queryparam .= "AND type"; +if($cat == 1) $queryparam .= " IN (2, 3, 8, 10, 11, 12, 13, 17, 18, 19)"; +elseif(isset($cats[$cat]["type"]) || $subcat) $queryparam .= " = ".($cats[$cat]["type"] ?? $subcat); +else $queryparam .= " IN (".implode(", ", $cats[$cat]["subcategories"]).")"; + +// process query parameters for the item price +$queryparam .= " AND price"; +if(is_numeric($currency) && $currency == 0) $queryparam .= " IS NOT NULL"; +elseif($currency == 2) $queryparam .= " = 0"; + +// get the number of assets matching the query +$results = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type != 1 AND name LIKE :keywd AND approved != 2 AND sale $queryparam", + [":keywd" => "%{$keyword}%"] +)->fetchColumn(); + +$pagination = Pagination($page, $results, 18); + +$query = Database::singleton()->run( + "SELECT assets.*, users.username FROM assets + INNER JOIN users ON creator = users.id + WHERE type != 1 AND name LIKE :keywd AND approved != 2 AND sale $queryparam + ORDER BY ".$sorts[$sort]." LIMIT 18 OFFSET :offset", + [":keywd" => "%{$keyword}%", ":offset" => $pagination->Offset] +); + +$pageBuilder = new PageBuilder(["title" => "Avatar Items, Virtual Avatars, Virtual Goods"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/catalog.js"); +$pageBuilder->buildHeader(); +?> + +=plural(Catalog::GetTypeByNum($type))?>
+ $cat_data) { ?> + + +Showing =number_format($pagination->Offset+1)?> - =number_format($pagination->Offset+$query->rowCount())?> of =number_format($results)?> results
+No results matched your criteria
+ +=Polygon::FilterText($item->name)?>
+ sale) { ?>=$item->price ? ' '.number_format($item->price):'Free'?>
+Creator: =$item->username?>
+Updated: =timeSince($item->updated)?>
+Sales: =number_format($item->Sales)?>
+`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `