diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9de68b3 --- /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..5b54d99 --- /dev/null +++ b/admin.php @@ -0,0 +1,138 @@ + (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/"), + "SetupUsage" => system::getFileSize(getSetupUsage([2009, 2010, 2011, 2012])) + ] +]; + +pageBuilder::$pageConfig["title"] = SITE_CONFIG["site"]["name"]." Administration"; +pageBuilder::buildHeader(); +?> + + +
'.users::getUserNameFromUid($row->userId).' - '.timeSince('@'.$row->timestamp).'
', + "message" => polygon::filterText($row->text) + ]; +} + +if($query->rowCount() < 15) +{ + $feed[] = + [ + "userName" => "Your feed is currently empty!", + "img" => "/img/feed-starter.png", + "header" => '=$text["header"][$banType]?>
+Done at: =date('j/n/Y g:i:s A \G\M\T')?>
+Reason:
+', '
', $markdown->text(trim($_POST["moderationNote"])))?> +
=$text["footer"][$banType]?>
+ +Reactivate + "POST", "admin" => true, "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::getItemInfo($assetID); + if(!$asset) api::respond(200, false, "The asset you requested does not exist"); + switch($asset->type) + { + 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 + default: api::respond(200, false, "This asset cannot be re-rendered"); + } +} +else if($renderType == "Avatar") +{ + $user = users::getUserInfoFromUid($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..8410208 --- /dev/null +++ b/api/admin/upload.php @@ -0,0 +1,110 @@ + "POST", "admin" => true, "secure" => true]); + +$file = $_FILES["file"] ?? false; +$name = $_POST["name"] ?? false; +$type = $_POST["type"] ?? false; +$uploadas = $_POST["creator"] ?? "Polygon"; +$creator = users::getUidFromUserName($uploadas); + +if(!$file) api::respond(200, false, "You must select a file"); +if(!$name) 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"); + + polygon::importLibrary("class.upload"); + + $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" => "", "approved" => 1]); + image::process($image, ["name" => "$imageId", "resize" => false, "dir" => "/asset/files/"]); + Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); +} +elseif($type == 3) // audio +{ + if(!in_array($file["type"], ["audio/mpeg", "audio/ogg", "audio/mid", "audio/wav", "video/ogg"])) 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" => "", "audioType" => $file["type"], "approved" => 1]); + copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId); + image::renderfromimg("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" => "", "approved" => 1]); + copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$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" => "", "approved" => 1]); + copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId); + image::renderfromimg("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 a .rbxm or .xml file"); + $assetId = catalog::createAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]); + copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$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 a .rbxm or .xml file"); + $assetId = catalog::createAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]); + copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$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"); + + polygon::importLibrary("class.upload"); + + $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" => "", "approved" => 1]); + image::process($image, ["name" => "$imageId", "resize" => false, "dir" => "/asset/files/"]); + Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = catalog::createAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "imageID" => $imageId, "approved" => 1]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, catalog::generateGraphicXML("Face", $imageId)); + + Thumbnails::UploadAsset($image, $itemId, 420, 230); + Thumbnails::UploadAsset($image, $itemId, 420, 420); + Thumbnails::UploadAsset($image, $itemId, 352, 352); + Thumbnails::UploadAsset($image, $itemId, 250, 250); + Thumbnails::UploadAsset($image, $itemId, 110, 110); + Thumbnails::UploadAsset($image, $itemId, 100, 100); + Thumbnails::UploadAsset($image, $itemId, 75, 75); + Thumbnails::UploadAsset($image, $itemId, 48, 48); +} +elseif($type == 19) //gear +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) api::respond(400, false, "Must be a .rbxm or .xml file"); + + $assetId = catalog::createAsset(["type" => $type, "creator" => $creator, "name" => $name, "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"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId); + polygon::requestRender("Model", $assetId); +} + +users::logStaffAction("[ Asset creation ] Created \"$name\" [ID ".($itemId ?? $assetId ?? $imageId)."]"); +api::respond_custom(["status" => 200, "success" => true, "message" => "".catalog::getTypeByNum($type)." successfully created!"]); \ 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..34ea1bc --- /dev/null +++ b/api/catalog/get-comments.php @@ -0,0 +1,36 @@ +prepare("SELECT COUNT(*) FROM asset_comments WHERE assetID = :id"); +$query->bindParam(":id", $assetID, PDO::PARAM_INT); +$query->execute(); + +$pages = ceil($query->fetchColumn()/15); +$offset = ($page - 1)*15; + +$query = $pdo->prepare("SELECT asset_comments.*, users.username FROM asset_comments INNER JOIN users ON users.id = asset_comments.author WHERE assetID = :id ORDER BY id DESC"); +$query->bindParam(":id", $assetID, PDO::PARAM_INT); +$query->execute(); +if(!$query->rowCount()) api::respond(200, true, "This asset has no comments"); + +$comments = []; + +while($row = $query->fetch(PDO::FETCH_OBJ)) +{ + $comments[] = + [ + "time" => strtolower(timeSince($row->time)), + "commenter_name" => $row->username, + "commenter_id" => $row->author, + "commenter_avatar" => Thumbnails::GetAvatar($row->author, 110, 110), + "content" => nl2br(polygon::filterText($row->content)) + ]; +} + +api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "comments" => $comments, "pages" => $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..1909892 --- /dev/null +++ b/api/catalog/post-comment.php @@ -0,0 +1,29 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST['assetID']) || !isset($_POST['content'])); + +$uid = SESSION["userId"]; +$id = $_POST['assetID']; +$content = $_POST['content']; + +$item = catalog::getItemInfo($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"); + +$query = $pdo->prepare("SELECT time FROM asset_comments WHERE time+60 > UNIX_TIMESTAMP() AND author = :uid"); +$query->bindParam(":uid", $uid, PDO::PARAM_INT); +$query->execute(); +$lastComment = $query->fetchColumn(); +if($lastComment) api::respond(400, false, "Please wait ".(60-(time()-$lastComment))." seconds before posting a new comment"); + +$query = $pdo->prepare("INSERT INTO asset_comments (author, content, assetID, time) VALUES (:uid, :content, :aid, UNIX_TIMESTAMP())"); +$query->bindParam(":uid", $uid, PDO::PARAM_INT); +$query->bindParam(":content", $content, PDO::PARAM_STR); +$query->bindParam(":aid", $id, PDO::PARAM_INT); +$query->execute(); + +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..9a05cbd --- /dev/null +++ b/api/catalog/purchase.php @@ -0,0 +1,59 @@ + "POST", "logged_in" => true, "secure" => true]); + +function getPrice($price) +{ + return $price ? ' '.$price.'' : 'Free'; +} + +$uid = SESSION["userId"]; +$id = $_POST['id'] ?? false; +$price = $_POST['price'] ?? 0; + +$item = catalog::getItemInfo($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["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["currency"] - $item->price), + "newprice" => $item->price + ])); + +$query = $pdo->prepare("UPDATE users SET currency = currency - :price WHERE id = :uid; UPDATE users SET currency = currency + :price WHERE id = :seller"); +$query->bindParam(":price", $item->price, PDO::PARAM_INT); +$query->bindParam(":uid", $uid, PDO::PARAM_INT); +$query->bindParam(":seller", $item->creator, PDO::PARAM_INT); +$query->execute(); + +$query = $pdo->prepare("INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())"); +$query->bindParam(":aid", $id, PDO::PARAM_INT); +$query->bindParam(":uid", $uid, PDO::PARAM_INT); +$query->execute(); + +$query = $pdo->prepare("INSERT INTO transactions (purchaser, seller, assetId, amount, timestamp) VALUES (:uid, :sid, :aid, :price, UNIX_TIMESTAMP())"); +$query->bindParam(":uid", $uid, PDO::PARAM_INT); +$query->bindParam(":sid", $item->creator, PDO::PARAM_INT); +$query->bindParam(":aid", $id, PDO::PARAM_INT); +$query->bindParam(":price", $item->price, PDO::PARAM_INT); +$query->execute(); + +die(json_encode( +[ + "status" => 200, + "success" => true, + "message" => "OK", + "header" => "Purchase Complete!", + "image" => Thumbnails::GetAsset($item, 110, 110), + "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']], +])); \ No newline at end of file diff --git a/api/develop/getCreations.php b/api/develop/getCreations.php new file mode 100644 index 0000000..42a70d4 --- /dev/null +++ b/api/develop/getCreations.php @@ -0,0 +1,34 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$type = $_POST["type"] ?? false; +$page = $_POST["page"] ?? 1; +$assets = []; + +if(!catalog::getTypeByNum($type)) api::respond(400, false, "Invalid asset type"); + +$query = $pdo->prepare("SELECT * FROM assets WHERE creator = :uid AND type = :type ORDER BY id DESC"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->bindParam(":type", $type, PDO::PARAM_INT); +$query->execute(); + +while($asset = $query->fetch(PDO::FETCH_OBJ)) +{ + $info = catalog::getItemInfo($asset->id); + + $assets[] = + [ + "name" => htmlspecialchars($asset->name), + "id" => $asset->id, + "thumbnail" => Thumbnails::GetAsset($asset, 110, 110), + "item_url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id, + "config_url" => "/my/item?ID=".$asset->id, + "created" => date("n/j/Y", $asset->created), + "sales-total" => $info->sales_total, + "sales-week" => $info->sales_week + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "assets" => $assets])); \ No newline at end of file diff --git a/api/develop/upload.php b/api/develop/upload.php new file mode 100644 index 0000000..283d7a9 --- /dev/null +++ b/api/develop/upload.php @@ -0,0 +1,123 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$file = $_FILES["file"] ?? false; +$name = $_POST["name"] ?? false; +$type = $_POST["type"] ?? false; + +if(!$file) api::respond(400, false, "You must select a file"); +if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) api::respond(400, false, "Must be a .png or .jpg file"); +if(!$name) api::respond(400, false, "You must specify a name"); +if(polygon::filterText($name, false, false, true) != $name) api::respond(400, false, "The name contains inappropriate text"); +if(!in_array($type, [2, 11, 12, 13])) api::respond(400, false, "You can't upload that type of content!"); + +$query = $pdo->prepare("SELECT created FROM assets WHERE creator = :uid ORDER BY id DESC"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->execute(); +$lastCreation = $query->fetchColumn(); +if($lastCreation+30 > time()) api::respond(400, 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) | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ + +polygon::importLibrary("class.upload"); + +$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["userId"], "name" => $name, "description" => catalog::getTypeByNum($type)." Image"]); + +if($type == 2) //tshirt +{ + image::process($image, ["name" => "$imageId", "keepRatio" => true, "align" => "T", "x" => 128, "y" => 128, "dir" => "/asset/files/"]); + Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "T"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "T"]); + + $itemId = catalog::createAsset(["type" => 2, "creator" => SESSION["userId"], "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::merge($template, $shirtdecal, 85, 85, 0, 0, 250, 250, 100); + + imagepng($template, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png"); + image::resize(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png", 100, 100, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-100x100.png"); + image::resize(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png", 110, 110, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-110x110.png"); + + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-100x100.png"); + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-110x110.png"); + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png"); +} +elseif($type == 11 || $type == 12) //shirt / pants +{ + image::process($image, ["name" => "$imageId", "x" => 585, "y" => 559, "dir" => "/asset/files/"]); + Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = catalog::createAsset(["type" => $type, "creator" => SESSION["userId"], "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); +} +elseif($type == 13) //decal +{ + image::process($image, ["name" => "$imageId", "x" => 256, "scaleY" => true, "dir" => "/asset/files/"]); + Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = catalog::createAsset(["type" => 13, "creator" => SESSION["userId"], "name" => $name, "description" => "Decal", "imageID" => $imageId]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, catalog::generateGraphicXML("Decal", $imageId)); + image::process($image, ["name" => "$itemId-48x48.png", "x" => 48, "y" => 48, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-75x75.png", "x" => 75, "y" => 75, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-100x100.png", "x" => 100, "y" => 100, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-110x110.png", "x" => 110, "y" => 110, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-250x250.png", "x" => 250, "y" => 250, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-352x352.png", "x" => 352, "y" => 352, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-420x230.png", "x" => 420, "y" => 230, "dir" => "/thumbs/assets/"]); + image::process($image, ["name" => "$itemId-420x420.png", "x" => 420, "y" => 420, "dir" => "/thumbs/assets/"]); +} + +api::respond_custom(["status" => 200, "success" => true, "message" => catalog::getTypeByNum($type)." successfully created!"]); \ No newline at end of file diff --git a/api/friends/accept.php b/api/friends/accept.php new file mode 100644 index 0000000..4591918 --- /dev/null +++ b/api/friends/accept.php @@ -0,0 +1,20 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$friendid = $_POST['friendID']; + +$query = $pdo->prepare("SELECT * FROM friends WHERE id = :id AND status = 0"); +$query->bindParam(":id", $friendid, PDO::PARAM_INT); +$query->execute(); +$friendInfo = $query->fetch(PDO::FETCH_OBJ); + +if(!$friendInfo) api::respond(400, false, "Friend request doesn't exist"); +if($friendInfo->receiverId != SESSION["userId"]) api::respond(400, false, "You are not the receiver of this friend request"); + +$query = $pdo->prepare("UPDATE friends SET status = 1 WHERE id = :id"); +$query->bindParam(":id", $friendid, PDO::PARAM_INT); +$query->execute(); + +api::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/friends/getFriendRequests.php b/api/friends/getFriendRequests.php new file mode 100644 index 0000000..1fbc6f0 --- /dev/null +++ b/api/friends/getFriendRequests.php @@ -0,0 +1,35 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$page = $_POST['page'] ?? 1; + +$query = $pdo->prepare("SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->execute(); + +$pages = ceil($query->fetchColumn()/18); +$offset = ($page - 1)*18; + +if(!$pages) api::respond(200, true, "You're all up-to-date with your friend requests!"); + +$query = $pdo->prepare("SELECT * FROM friends WHERE receiverId = :uid AND status = 0 LIMIT 18 OFFSET :offset"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->bindParam(":offset", $offset, PDO::PARAM_INT); +$query->execute(); + +$friends = []; + +while($row = $query->fetch(PDO::FETCH_OBJ)) +{ + $friends[] = + [ + "username" => users::getUserNameFromUid($row->requesterId), + "userid" => $row->requesterId, + "avatar" => Thumbnails::GetAvatar($row->requesterId, 250, 250), + "friendid" => $row->id + ]; +} + +api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "requests" => $friends, "pages" => $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..9311d6b --- /dev/null +++ b/api/friends/getFriends.php @@ -0,0 +1,47 @@ + "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::getUserInfoFromUid($userId)) api::respond(400, false, "User does not exist"); + +$query = $pdo->prepare("SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1"); +$query->bindParam(":uid", $userId, PDO::PARAM_INT); +$query->execute(); + +$pages = ceil($query->fetchColumn()/$limit); +$offset = ($page - 1)*$limit; + +if(!$pages) api::respond(200, true, ($self ? "You do" : users::getUserNameFromUid($userId)." does")."n't have any friends"); + +$query = $pdo->prepare(" + 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"); +$query->bindParam(":uid", $userId, PDO::PARAM_INT); +$query->bindParam(":limit", $limit, PDO::PARAM_INT); +$query->bindParam(":offset", $offset, PDO::PARAM_INT); +$query->execute(); + +$friends = []; + +while($row = $query->fetch(PDO::FETCH_OBJ)) +{ + $friends[] = + [ + "username" => $row->username, + "userid" => $row->userId, + "avatar" => Thumbnails::GetAvatar($row->userId, 250, 250), + "friendid" => $row->id, + "status" => polygon::filterText($row->status) + ]; +} + +api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "friends" => $friends, "pages" => $pages]); \ No newline at end of file diff --git a/api/friends/revoke.php b/api/friends/revoke.php new file mode 100644 index 0000000..6e73516 --- /dev/null +++ b/api/friends/revoke.php @@ -0,0 +1,20 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$friendid = $_POST['friendID']; + +$query = $pdo->prepare("SELECT * FROM friends WHERE id = :id AND NOT status = 2"); +$query->bindParam(":id", $friendid, PDO::PARAM_INT); +$query->execute(); +$friendInfo = $query->fetch(PDO::FETCH_OBJ); + +if(!$friendInfo) api::respond(400, false, "Friend connection doesn't exist"); +if(!in_array($userid, [$friendInfo->requesterId, $friendInfo->receiverId])) api::respond(400, false, "You are not a part of this friend connection"); + +$query = $pdo->prepare("UPDATE friends SET status = 2 WHERE id = :id"); +$query->bindParam(":id", $friendid, PDO::PARAM_INT); +$query->execute(); + +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..1bdca36 --- /dev/null +++ b/api/friends/send.php @@ -0,0 +1,27 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["userId"]; +$friendid = $_POST['userID'] ?? false; + +if(!$friendid) api::respond(400, false, "Bad Request"); +if($friendid == $userid) api::respond(400, false, "You can't perform friend operations on yourself"); + +$query = $pdo->prepare("SELECT status FROM friends WHERE :uid IN (requesterId, receiverId) AND :rid IN (requesterId, receiverId) AND NOT status = 2"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->bindParam(":rid", $friendid, PDO::PARAM_INT); +$query->execute(); +if($query->rowCount()) api::respond(400, false, "Friend connection already exists"); + +$query = $pdo->prepare("SELECT timeSent FROM friends WHERE requesterId = :uid AND timeSent+30 > UNIX_TIMESTAMP()"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->execute(); +if($query->rowCount()) api::respond(400, false, "Please wait ".(($query->fetchColumn()+30)-time())." seconds before sending another request"); + +$query = $pdo->prepare("INSERT INTO friends (requesterId, receiverId, timeSent) VALUES (:uid, :rid, UNIX_TIMESTAMP())"); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->bindParam(":rid", $friendid, PDO::PARAM_INT); +$query->execute(); + +api::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/games/getServers.php b/api/games/getServers.php new file mode 100644 index 0000000..e49f698 --- /dev/null +++ b/api/games/getServers.php @@ -0,0 +1,61 @@ + "POST"]); + +$client = $_POST["client"] ?? "false"; +$creator = $_POST["creator"] ?? false; +$page = $_POST["page"] ?? 1; +$pages = 1; +$items = []; + +$query_params = "1"; +$value_params = []; + +if($client !== "false") +{ + if(!in_array($client, [2009, 2010, 2011, 2012])) api::respond(400, false, "Bad Request"); + $query_params .= " AND version = :version"; + $value_params[":version"] = $client; +} + +if($creator) +{ + $query_params .= " AND hoster = :uid"; + $value_params[":uid"] = $creator; +} + +$servercount = db::run("SELECT COUNT(*) FROM selfhosted_servers WHERE $query_params", $value_params)->fetchColumn(); +$pages = ceil($servercount/10); +$offset = ($page - 1)*10; + +$servers = db::run(" + SELECT *, + (SELECT COUNT(*) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid) AS players, + (ping+35 > UNIX_TIMESTAMP()) AS online + FROM selfhosted_servers WHERE $query_params + ORDER BY online DESC, players DESC, ping DESC, created DESC LIMIT 10 OFFSET $offset", $value_params); + +if(!$servers->rowCount()) api::respond(200, true, "No servers matched your query"); +while($server = $servers->fetch(PDO::FETCH_OBJ)) +{ + $gears = []; + foreach(json_decode($server->allowed_gears, true) as $gear_attr => $gear_val) + if($gear_val) $gears[] = ["name" => catalog::$gear_attr_display[$gear_attr]["text_sel"], "icon" => catalog::$gear_attr_display[$gear_attr]["icon"]]; + $items[] = + [ + "server_name" => polygon::filterText($server->name), + "server_id" => $server->id, + "server_thumbnail" => Thumbnails::GetAvatar($server->hoster, 420, 420), + "hoster_name" => users::getUserNameFromUid($server->hoster), + "hoster_id" => $server->hoster, + "date" => date('n/d/Y g:i:s A', $server->created), + "version" => $server->version, + "server_online" => $server->ping+35 > time() ? true : false, + "players_online" =>$server->ping+35 > time() ? $server->players : 0, + "players_max" => $server->maxplayers, + "gears" => $gears + ]; +} + + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "items" => $items])); \ No newline at end of file diff --git a/api/games/serverlauncher.php b/api/games/serverlauncher.php new file mode 100644 index 0000000..ce9b378 --- /dev/null +++ b/api/games/serverlauncher.php @@ -0,0 +1,62 @@ + "GET"]);//, "logged_in" => true, "secure" => true]); + +if(!SITE_CONFIG["site"]["games"]) api::respond(200, false, "Games are temporarily disabled for maintenance"); + +$serverID = $_GET["serverID"] ?? $_GET['placeId'] ?? false; +$isTeleport = isset($_GET["isTeleport"]) && $_GET['isTeleport'] == "true"; + +if($isTeleport && $_SERVER["HTTP_USER_AGENT"] != "Roblox/WinInet") + api::respond_custom([ + "Error" => "Request is not authorized from specified origin", + "userAgent" => $_SERVER["HTTP_USER_AGENT"] ?? null, + "referrer" => $_SERVER["HTTP_REFERER"] ?? null + ]); + +$query = $pdo->prepare("SELECT *, (SELECT COUNT(*) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid) AS players FROM selfhosted_servers WHERE id = :sid"); +$query->bindParam(":sid", $serverID, PDO::PARAM_INT); +$query->execute(); +$serverInfo = $query->fetch(PDO::FETCH_OBJ); + +if(!$serverInfo) api::respond(400, false, "Server does not exist"); +if($serverInfo->players >= $serverInfo->maxplayers) api::respond(200, false, "This server is currently full. Please try again later"); + +if($isTeleport) +{ + $ticket = $_COOKIE['ticket'] ?? false; + $query = $pdo->prepare("SELECT uid FROM client_sessions WHERE ticket = :ticket"); + $query->bindParam(":ticket", $ticket, PDO::PARAM_STR); + $query->execute(); + if(!$query->rowCount()) api::respond_custom(["Error" => "You are not logged in"]); + $userid = $query->fetchColumn(); +} +else +{ + if(!SESSION) api::respond(400, false, "You are not logged in"); + $userid = SESSION["userId"]; +} + +$ticket = generateUUID(); +$securityTicket = generateUUID(); +$query = $pdo->prepare("INSERT INTO client_sessions (ticket, securityTicket, uid, sessionType, serverID, created, isTeleport) VALUES (:uuid, :security, :uid, 1, :sid, UNIX_TIMESTAMP(), :teleport)"); +$query->bindParam(":uuid", $ticket, PDO::PARAM_STR); +$query->bindParam(":security", $securityTicket, PDO::PARAM_STR); +$query->bindParam(":uid", $userid, PDO::PARAM_INT); +$query->bindParam(":sid", $serverID, PDO::PARAM_INT); +$query->bindParam(":teleport", $isTeleport, PDO::PARAM_INT); +$query->execute(); + +api::respond_custom([ + "status" => 200, + "success" => true, + "message" => "OK", + "version" => $serverInfo->version, + "joinScriptUrl" => "http://chef.pizzaboxer.xyz/game/join?ticket=".$ticket, + // these last few params are for teleportservice and lack any function - just ignore + "authenticationUrl" => "http://chef.pizzaboxer.xyz/Login/Negotiate.ashx", + "authenticationTicket" => "unusedplzignore", + "status" => 2 +]); \ No newline at end of file diff --git a/api/ide/toolbox.php b/api/ide/toolbox.php new file mode 100644 index 0000000..56b6f49 --- /dev/null +++ b/api/ide/toolbox.php @@ -0,0 +1,118 @@ +"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; +$keywd_sql = $keywd ? "%".$keywd."%" : "%"; + +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 + { + $userId = SESSION["userId"]; + $query = $pdo->prepare("SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND id IN (SELECT assetId FROM ownedAssets WHERE userId = :uid)"); + $query->bindParam(":uid", $userId, PDO::PARAM_INT); + } + else //get assets from catalog + { + $query = $pdo->prepare("SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q)"); + $query->bindParam(":q", $keywd_sql, PDO::PARAM_STR); + } + $query->bindParam(":type", $type, PDO::PARAM_INT); +} + +$query->execute(); +$items = $query->fetchColumn(); +$pages = ceil($items/20); +$offset = ($page - 1)*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 + { + $userId = SESSION["userId"]; + $query = $pdo->prepare("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"); //all of this just to order by time bought... + $query->bindParam(":uid", $userId, PDO::PARAM_INT); + } + else //get assets from catalog + { + $query = $pdo->prepare("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"); + $query->bindParam(":q", $keywd_sql, PDO::PARAM_STR); + $query->bindParam(":q2", $keywd_sql, PDO::PARAM_STR); + } + $query->bindParam(":type", $type, PDO::PARAM_INT); +} + +$query->bindParam(":offset", $offset, PDO::PARAM_INT); +$query->execute(); +?> +
+ ') + { + $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/vendors/PasswordLock.php b/api/private/vendors/PasswordLock.php new file mode 100644 index 0000000..a4e8f74 --- /dev/null +++ b/api/private/vendors/PasswordLock.php @@ -0,0 +1,1332 @@ +key_bytes + ); + } + + public function getRawBytes() + { + return $this->key_bytes; + } + + private function __construct($bytes) + { + Core::ensureTrue( + Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE, + 'Bad key length.' + ); + $this->key_bytes = $bytes; + } + + } + + final class KeyOrPassword + { + const PBKDF2_ITERATIONS = 100000; + const SECRET_TYPE_KEY = 1; + const SECRET_TYPE_PASSWORD = 2; + + private $secret_type = 0; + + private $secret; + + public static function createFromKey(Key $key) + { + return new KeyOrPassword(self::SECRET_TYPE_KEY, $key); + } + + public static function createFromPassword($password) + { + return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password); + } + + public function deriveKeys($salt) + { + Core::ensureTrue( + Core::ourStrlen($salt) === Core::SALT_BYTE_SIZE, + 'Bad salt.' + ); + + if ($this->secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($this->secret instanceof Key); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $akey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $this->secret->getRawBytes(), + Core::KEY_BYTE_SIZE, + Core::AUTHENTICATION_INFO_STRING, + $salt + ); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $ekey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $this->secret->getRawBytes(), + Core::KEY_BYTE_SIZE, + Core::ENCRYPTION_INFO_STRING, + $salt + ); + return new DerivedKeys($akey, $ekey); + } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($this->secret)); + /* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in + * GitHub issue #230. The fix is to pre-hash the password to ensure + * it is short. We do the prehashing here instead of in pbkdf2() so + * that pbkdf2() still computes the function as defined by the + * standard. */ + + /** + * @psalm-suppress PossiblyInvalidArgument + */ + $prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true); + + $prekey = Core::pbkdf2( + Core::HASH_FUNCTION_NAME, + $prehash, + $salt, + self::PBKDF2_ITERATIONS, + Core::KEY_BYTE_SIZE, + true + ); + $akey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $prekey, + Core::KEY_BYTE_SIZE, + Core::AUTHENTICATION_INFO_STRING, + $salt + ); + /* Note the cryptographic re-use of $salt here. */ + $ekey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $prekey, + Core::KEY_BYTE_SIZE, + Core::ENCRYPTION_INFO_STRING, + $salt + ); + return new DerivedKeys($akey, $ekey); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } + } + + private function __construct($secret_type, $secret) + { + // The constructor is private, so these should never throw. + if ($secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($secret instanceof Key); + } elseif ($secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($secret)); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } + $this->secret_type = $secret_type; + $this->secret = $secret; + } + } + + final class Core + { + const HEADER_VERSION_SIZE = 4; + const MINIMUM_CIPHERTEXT_SIZE = 84; + + const CURRENT_VERSION = "\xDE\xF5\x02\x00"; + + const CIPHER_METHOD = 'aes-256-ctr'; + const BLOCK_BYTE_SIZE = 16; + const KEY_BYTE_SIZE = 32; + const SALT_BYTE_SIZE = 32; + const MAC_BYTE_SIZE = 32; + const HASH_FUNCTION_NAME = 'sha256'; + const ENCRYPTION_INFO_STRING = 'DefusePHP|V2|KeyForEncryption'; + const AUTHENTICATION_INFO_STRING = 'DefusePHP|V2|KeyForAuthentication'; + const BUFFER_BYTE_SIZE = 1048576; + + const LEGACY_CIPHER_METHOD = 'aes-128-cbc'; + const LEGACY_BLOCK_BYTE_SIZE = 16; + const LEGACY_KEY_BYTE_SIZE = 16; + const LEGACY_HASH_FUNCTION_NAME = 'sha256'; + const LEGACY_MAC_BYTE_SIZE = 32; + const LEGACY_ENCRYPTION_INFO_STRING = 'DefusePHP|KeyForEncryption'; + const LEGACY_AUTHENTICATION_INFO_STRING = 'DefusePHP|KeyForAuthentication'; + + public static function incrementCounter($ctr, $inc) + { + Core::ensureTrue( + Core::ourStrlen($ctr) === Core::BLOCK_BYTE_SIZE, + 'Trying to increment a nonce of the wrong size.' + ); + + Core::ensureTrue( + \is_int($inc), + 'Trying to increment nonce by a non-integer.' + ); + + // The caller is probably re-using CTR-mode keystream if they increment by 0. + Core::ensureTrue( + $inc > 0, + 'Trying to increment a nonce by a nonpositive amount' + ); + + Core::ensureTrue( + $inc <= PHP_INT_MAX - 255, + 'Integer overflow may occur' + ); + + /* + * We start at the rightmost byte (big-endian) + * So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584 + */ + for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) { + $sum = \ord($ctr[$i]) + $inc; + + /* Detect integer overflow and fail. */ + Core::ensureTrue(\is_int($sum), 'Integer overflow in CTR mode nonce increment'); + + $ctr[$i] = \pack('C', $sum & 0xFF); + $inc = $sum >> 8; + } + return $ctr; + } + + public static function secureRandom($octets) + { + self::ensureFunctionExists('random_bytes'); + try { + return \random_bytes($octets); + } catch (\Exception $ex) { + throw new Ex\EnvironmentIsBrokenException( + 'Your system does not have a secure random number generator.' + ); + } + } + + public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) + { + static $nativeHKDF = null; + if ($nativeHKDF === null) { + $nativeHKDF = \is_callable('\\hash_hkdf'); + } + if ($nativeHKDF) { + if (\is_null($salt)) { + $salt = ''; + } + return \hash_hkdf($hash, $ikm, $length, $info, $salt); + } + + $digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true)); + + // Sanity-check the desired output length. + Core::ensureTrue( + !empty($length) && \is_int($length) && $length >= 0 && $length <= 255 * $digest_length, + 'Bad output length requested of HDKF.' + ); + + // "if [salt] not provided, is set to a string of HashLen zeroes." + if (\is_null($salt)) { + $salt = \str_repeat("\x00", $digest_length); + } + + // HKDF-Extract: + // PRK = HMAC-Hash(salt, IKM) + // The salt is the HMAC key. + $prk = \hash_hmac($hash, $ikm, $salt, true); + + // HKDF-Expand: + + // This check is useless, but it serves as a reminder to the spec. + Core::ensureTrue(Core::ourStrlen($prk) >= $digest_length); + + // T(0) = '' + $t = ''; + $last_block = ''; + for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) { + // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) + $last_block = \hash_hmac( + $hash, + $last_block . $info . \chr($block_index), + $prk, + true + ); + // T = T(1) | T(2) | T(3) | ... | T(N) + $t .= $last_block; + } + + // ORM = first L octets of T + /** @var string $orm */ + $orm = Core::ourSubstr($t, 0, $length); + Core::ensureTrue(\is_string($orm)); + return $orm; + } + + public static function hashEquals($expected, $given) + { + static $native = null; + if ($native === null) { + $native = \function_exists('hash_equals'); + } + if ($native) { + return \hash_equals($expected, $given); + } + + // We can't just compare the strings with '==', since it would make + // timing attacks possible. We could use the XOR-OR constant-time + // comparison algorithm, but that may not be a reliable defense in an + // interpreted language. So we use the approach of HMACing both strings + // with a random key and comparing the HMACs. + + // We're not attempting to make variable-length string comparison + // secure, as that's very difficult. Make sure the strings are the same + // length. + Core::ensureTrue(Core::ourStrlen($expected) === Core::ourStrlen($given)); + + $blind = Core::secureRandom(32); + $message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind); + $correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind); + return $correct_compare === $message_compare; + } + + public static function ensureConstantExists($name) + { + Core::ensureTrue(\defined($name)); + } + + public static function ensureFunctionExists($name) + { + Core::ensureTrue(\function_exists($name)); + } + + public static function ensureTrue($condition, $message = '') + { + if (!$condition) { + throw new Ex\EnvironmentIsBrokenException($message); + } + } + + /* + * We need these strlen() and substr() functions because when + * 'mbstring.func_overload' is set in php.ini, the standard strlen() and + * substr() are replaced by mb_strlen() and mb_substr(). + */ + + public static function ourStrlen($str) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + if ($exists) { + $length = \mb_strlen($str, '8bit'); + Core::ensureTrue($length !== false); + return $length; + } else { + return \strlen($str); + } + } + + public static function ourSubstr($str, $start, $length = null) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + + // This is required to make mb_substr behavior identical to substr. + // Without this, mb_substr() would return false, contra to what the + // PHP documentation says (it doesn't say it can return false.) + $input_len = Core::ourStrlen($str); + if ($start === $input_len && !$length) { + return ''; + } + + if ($start > $input_len) { + return false; + } + + // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP 5.3, + // so we have to find the length ourselves. Also, substr() doesn't + // accept null for the length. + if (! isset($length)) { + if ($start >= 0) { + $length = $input_len - $start; + } else { + $length = -$start; + } + } + + if ($length < 0) { + throw new \InvalidArgumentException( + "Negative lengths are not supported with ourSubstr." + ); + } + + if ($exists) { + $substr = \mb_substr($str, $start, $length, '8bit'); + // At this point there are two cases where mb_substr can + // legitimately return an empty string. Either $length is 0, or + // $start is equal to the length of the string (both mb_substr and + // substr return an empty string when this happens). It should never + // ever return a string that's longer than $length. + if (Core::ourStrlen($substr) > $length || (Core::ourStrlen($substr) === 0 && $length !== 0 && $start !== $input_len)) { + throw new Ex\EnvironmentIsBrokenException( + 'Your version of PHP has bug #66797. Its implementation of + mb_substr() is incorrect. See the details here: + https://bugs.php.net/bug.php?id=66797' + ); + } + return $substr; + } + + return \substr($str, $start, $length); + } + + public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) + { + // Type checks: + if (! \is_string($algorithm)) { + throw new \InvalidArgumentException( + 'pbkdf2(): algorithm must be a string' + ); + } + if (! \is_string($password)) { + throw new \InvalidArgumentException( + 'pbkdf2(): password must be a string' + ); + } + if (! \is_string($salt)) { + throw new \InvalidArgumentException( + 'pbkdf2(): salt must be a string' + ); + } + // Coerce strings to integers with no information loss or overflow + $count += 0; + $key_length += 0; + + $algorithm = \strtolower($algorithm); + Core::ensureTrue( + \in_array($algorithm, \hash_algos(), true), + 'Invalid or unsupported hash algorithm.' + ); + + // Whitelist, or we could end up with people using CRC32. + $ok_algorithms = [ + 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', + 'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool', + ]; + Core::ensureTrue( + \in_array($algorithm, $ok_algorithms, true), + 'Algorithm is not a secure cryptographic hash function.' + ); + + Core::ensureTrue($count > 0 && $key_length > 0, 'Invalid PBKDF2 parameters.'); + + if (\function_exists('hash_pbkdf2')) { + // The output length is in NIBBLES (4-bits) if $raw_output is false! + if (! $raw_output) { + $key_length = $key_length * 2; + } + return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); + } + + $hash_length = Core::ourStrlen(\hash($algorithm, '', true)); + $block_count = \ceil($key_length / $hash_length); + + $output = ''; + for ($i = 1; $i <= $block_count; $i++) { + // $i encoded as 4 bytes, big endian. + $last = $salt . \pack('N', $i); + // first iteration + $last = $xorsum = \hash_hmac($algorithm, $last, $password, true); + // perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) { + $xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true)); + } + $output .= $xorsum; + } + + if ($raw_output) { + return (string) Core::ourSubstr($output, 0, $key_length); + } else { + return Encoding::binToHex((string) Core::ourSubstr($output, 0, $key_length)); + } + } + } + + final class Encoding + { + const CHECKSUM_BYTE_SIZE = 32; + const CHECKSUM_HASH_ALGO = 'sha256'; + const SERIALIZE_HEADER_BYTES = 4; + + public static function binToHex($byte_string) + { + $hex = ''; + $len = Core::ourStrlen($byte_string); + for ($i = 0; $i < $len; ++$i) { + $c = \ord($byte_string[$i]) & 0xf; + $b = \ord($byte_string[$i]) >> 4; + $hex .= \pack( + 'CC', + 87 + $b + ((($b - 10) >> 8) & ~38), + 87 + $c + ((($c - 10) >> 8) & ~38) + ); + } + return $hex; + } + + public static function hexToBin($hex_string) + { + $hex_pos = 0; + $bin = ''; + $hex_len = Core::ourStrlen($hex_string); + $state = 0; + $c_acc = 0; + + while ($hex_pos < $hex_len) { + $c = \ord($hex_string[$hex_pos]); + $c_num = $c ^ 48; + $c_num0 = ($c_num - 10) >> 8; + $c_alpha = ($c & ~32) - 55; + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + if (($c_num0 | $c_alpha0) === 0) { + throw new Ex\BadFormatException( + 'Encoding::hexToBin() input is not a hex string.' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \pack('C', $c_acc | $c_val); + } + $state ^= 1; + ++$hex_pos; + } + return $bin; + } + + public static function trimTrailingWhitespace($string = '') + { + $length = Core::ourStrlen($string); + if ($length < 1) { + return ''; + } + do { + $prevLength = $length; + $last = $length - 1; + $chr = \ord($string[$last]); + + /* Null Byte (0x00), a.k.a. \0 */ + // if ($chr === 0x00) $length -= 1; + $sub = (($chr - 1) >> 8 ) & 1; + $length -= $sub; + $last -= $sub; + + /* Horizontal Tab (0x09) a.k.a. \t */ + $chr = \ord($string[$last]); + // if ($chr === 0x09) $length -= 1; + $sub = (((0x08 - $chr) & ($chr - 0x0a)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* New Line (0x0a), a.k.a. \n */ + $chr = \ord($string[$last]); + // if ($chr === 0x0a) $length -= 1; + $sub = (((0x09 - $chr) & ($chr - 0x0b)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Carriage Return (0x0D), a.k.a. \r */ + $chr = \ord($string[$last]); + // if ($chr === 0x0d) $length -= 1; + $sub = (((0x0c - $chr) & ($chr - 0x0e)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Space */ + $chr = \ord($string[$last]); + // if ($chr === 0x20) $length -= 1; + $sub = (((0x1f - $chr) & ($chr - 0x21)) >> 8) & 1; + $length -= $sub; + } while ($prevLength !== $length && $length > 0); + return (string) Core::ourSubstr($string, 0, $length); + } + + /* + * SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS: + * + * The checksum introduces a potential security weakness. For example, + * suppose we apply a checksum to a key, and that an adversary has an + * exploit against the process containing the key, such that they can + * overwrite an arbitrary byte of memory and then cause the checksum to + * be verified and learn the result. + * + * In this scenario, the adversary can extract the key one byte at + * a time by overwriting it with their guess of its value and then + * asking if the checksum matches. If it does, their guess was right. + * This kind of attack may be more easy to implement and more reliable + * than a remote code execution attack. + * + * This attack also applies to authenticated encryption as a whole, in + * the situation where the adversary can overwrite a byte of the key + * and then cause a valid ciphertext to be decrypted, and then + * determine whether the MAC check passed or failed. + * + * By using the full SHA256 hash instead of truncating it, I'm ensuring + * that both ways of going about the attack are equivalently difficult. + * A shorter checksum of say 32 bits might be more useful to the + * adversary as an oracle in case their writes are coarser grained. + * + * Because the scenario assumes a serious vulnerability, we don't try + * to prevent attacks of this style. + */ + + public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) + { + // Headers must be a constant length to prevent one type's header from + // being a prefix of another type's header, leading to ambiguity. + Core::ensureTrue( + Core::ourStrlen($header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' + ); + + return Encoding::binToHex( + $header . + $bytes . + \hash( + self::CHECKSUM_HASH_ALGO, + $header . $bytes, + true + ) + ); + } + + public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string) + { + // Headers must be a constant length to prevent one type's header from + // being a prefix of another type's header, leading to ambiguity. + Core::ensureTrue( + Core::ourStrlen($expected_header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be 4 bytes.' + ); + + /* If you get an exception here when attempting to load from a file, first pass your + key to Encoding::trimTrailingWhitespace() to remove newline characters, etc. */ + $bytes = Encoding::hexToBin($string); + + /* Make sure we have enough bytes to get the version header and checksum. */ + if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) { + throw new Ex\BadFormatException( + 'Encoded data is shorter than expected.' + ); + } + + /* Grab the version header. */ + $actual_header = (string) Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES); + + if ($actual_header !== $expected_header) { + throw new Ex\BadFormatException( + 'Invalid header.' + ); + } + + /* Grab the bytes that are part of the checksum. */ + $checked_bytes = (string) Core::ourSubstr( + $bytes, + 0, + Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE + ); + + /* Grab the included checksum. */ + $checksum_a = (string) Core::ourSubstr( + $bytes, + Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE, + self::CHECKSUM_BYTE_SIZE + ); + + /* Re-compute the checksum. */ + $checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true); + + /* Check if the checksum matches. */ + if (! Core::hashEquals($checksum_a, $checksum_b)) { + throw new Ex\BadFormatException( + "Data is corrupted, the checksum doesn't match" + ); + } + + return (string) Core::ourSubstr( + $bytes, + self::SERIALIZE_HEADER_BYTES, + Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE + ); + } + } + + final class DerivedKeys + { + private $akey = ''; + + private $ekey = ''; + + public function getAuthenticationKey() + { + return $this->akey; + } + + public function getEncryptionKey() + { + return $this->ekey; + } + + public function __construct($akey, $ekey) + { + $this->akey = $akey; + $this->ekey = $ekey; + } + } + + class Crypto + { + public static function encrypt($plaintext, $key, $raw_binary = false) + { + if (!\is_string($plaintext)) { + throw new \TypeError( + 'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.' + ); + } + if (!($key instanceof Key)) { + throw new \TypeError( + 'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.' + ); + } + if (!\is_bool($raw_binary)) { + throw new \TypeError( + 'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.' + ); + } + return self::encryptInternal( + $plaintext, + KeyOrPassword::createFromKey($key), + $raw_binary + ); + } + + public static function encryptWithPassword($plaintext, $password, $raw_binary = false) + { + if (!\is_string($plaintext)) { + throw new \TypeError( + 'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.' + ); + } + if (!\is_string($password)) { + throw new \TypeError( + 'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.' + ); + } + if (!\is_bool($raw_binary)) { + throw new \TypeError( + 'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.' + ); + } + return self::encryptInternal( + $plaintext, + KeyOrPassword::createFromPassword($password), + $raw_binary + ); + } + + public static function decrypt($ciphertext, $key, $raw_binary = false) + { + if (!\is_string($ciphertext)) { + throw new \TypeError( + 'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.' + ); + } + if (!($key instanceof Key)) { + throw new \TypeError( + 'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.' + ); + } + if (!\is_bool($raw_binary)) { + throw new \TypeError( + 'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.' + ); + } + return self::decryptInternal( + $ciphertext, + KeyOrPassword::createFromKey($key), + $raw_binary + ); + } + + public static function decryptWithPassword($ciphertext, $password, $raw_binary = false) + { + if (!\is_string($ciphertext)) { + throw new \TypeError( + 'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.' + ); + } + if (!\is_string($password)) { + throw new \TypeError( + 'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.' + ); + } + if (!\is_bool($raw_binary)) { + throw new \TypeError( + 'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.' + ); + } + return self::decryptInternal( + $ciphertext, + KeyOrPassword::createFromPassword($password), + $raw_binary + ); + } + + private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary) + { + RuntimeTests::runtimeTest(); + + $salt = Core::secureRandom(Core::SALT_BYTE_SIZE); + $keys = $secret->deriveKeys($salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + $iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE); + + $ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv); + $auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true); + $ciphertext = $ciphertext . $auth; + + if ($raw_binary) { + return $ciphertext; + } + return Encoding::binToHex($ciphertext); + } + + private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary) + { + RuntimeTests::runtimeTest(); + + if (! $raw_binary) { + try { + $ciphertext = Encoding::hexToBin($ciphertext); + } catch (Ex\BadFormatException $ex) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Ciphertext has invalid hex encoding.' + ); + } + } + + if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Ciphertext is too short.' + ); + } + + // Get and check the version header. + /** @var string $header */ + $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); + if ($header !== Core::CURRENT_VERSION) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Bad version header.' + ); + } + + // Get the salt. + /** @var string $salt */ + $salt = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE, + Core::SALT_BYTE_SIZE + ); + Core::ensureTrue(\is_string($salt)); + + // Get the IV. + /** @var string $iv */ + $iv = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, + Core::BLOCK_BYTE_SIZE + ); + Core::ensureTrue(\is_string($iv)); + + // Get the HMAC. + /** @var string $hmac */ + $hmac = Core::ourSubstr( + $ciphertext, + Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, + Core::MAC_BYTE_SIZE + ); + Core::ensureTrue(\is_string($hmac)); + + // Get the actual encrypted ciphertext. + /** @var string $encrypted */ + $encrypted = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + + Core::BLOCK_BYTE_SIZE, + Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - + Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE + ); + Core::ensureTrue(\is_string($encrypted)); + + // Derive the separate encryption and authentication keys from the key + // or password, whichever it is. + $keys = $secret->deriveKeys($salt); + + if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) { + $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD); + return $plaintext; + } else { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Integrity check failed.' + ); + } + } + + protected static function plainEncrypt($plaintext, $key, $iv) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_encrypt'); + /** @var string $ciphertext */ + $ciphertext = \openssl_encrypt( + $plaintext, + Core::CIPHER_METHOD, + $key, + OPENSSL_RAW_DATA, + $iv + ); + + Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed'); + + return $ciphertext; + } + + protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_decrypt'); + + /** @var string $plaintext */ + $plaintext = \openssl_decrypt( + $ciphertext, + $cipherMethod, + $key, + OPENSSL_RAW_DATA, + $iv + ); + Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.'); + + return $plaintext; + } + + protected static function verifyHMAC($expected_hmac, $message, $key) + { + $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); + return Core::hashEquals($message_hmac, $expected_hmac); + } + } + + class RuntimeTests extends Crypto + { + public static function runtimeTest() + { + // 0: Tests haven't been run yet. + // 1: Tests have passed. + // 2: Tests are running right now. + // 3: Tests have failed. + static $test_state = 0; + + if ($test_state === 1 || $test_state === 2) { + return; + } + + if ($test_state === 3) { + /* If an intermittent problem caused a test to fail previously, we + * want that to be indicated to the user with every call to this + * library. This way, if the user first does something they really + * don't care about, and just ignores all exceptions, they won't get + * screwed when they then start to use the library for something + * they do care about. */ + throw new Ex\EnvironmentIsBrokenException('Tests failed previously.'); + } + + try { + $test_state = 2; + + Core::ensureFunctionExists('openssl_get_cipher_methods'); + if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) { + throw new Ex\EnvironmentIsBrokenException( + 'Cipher method not supported. This is normally caused by an outdated ' . + 'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' . + 'Please upgrade to a newer version of OpenSSL that supports ' . + Core::CIPHER_METHOD . ' to use this library.' + ); + } + + RuntimeTests::AESTestVector(); + RuntimeTests::HMACTestVector(); + RuntimeTests::HKDFTestVector(); + + RuntimeTests::testEncryptDecrypt(); + Core::ensureTrue(Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) === Core::KEY_BYTE_SIZE); + + Core::ensureTrue(Core::ENCRYPTION_INFO_STRING !== Core::AUTHENTICATION_INFO_STRING); + } catch (Ex\EnvironmentIsBrokenException $ex) { + // Do this, otherwise it will stay in the "tests are running" state. + $test_state = 3; + throw $ex; + } + + // Change this to '0' make the tests always re-run (for benchmarking). + $test_state = 1; + } + + private static function testEncryptDecrypt() + { + $key = Key::createNewRandomKey(); + $data = "EnCrYpT EvErYThInG\x00\x00"; + + // Make sure encrypting then decrypting doesn't change the message. + $ciphertext = Crypto::encrypt($data, $key, true); + try { + $decrypted = Crypto::decrypt($ciphertext, $key, true); + } catch (Ex\WrongKeyOrModifiedCiphertextException $ex) { + // It's important to catch this and change it into a + // Ex\EnvironmentIsBrokenException, otherwise a test failure could trick + // the user into thinking it's just an invalid ciphertext! + throw new Ex\EnvironmentIsBrokenException(); + } + Core::ensureTrue($decrypted === $data); + + // Modifying the ciphertext: Appending a string. + try { + Crypto::decrypt($ciphertext . 'a', $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Modifying the ciphertext: Changing an HMAC byte. + $indices_to_change = [ + 0, // The header. + Core::HEADER_VERSION_SIZE + 1, // the salt + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext + ]; + + foreach ($indices_to_change as $index) { + try { + $ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256); + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + // Decrypting with the wrong key. + $key = Key::createNewRandomKey(); + $data = 'abcdef'; + $ciphertext = Crypto::encrypt($data, $key, true); + $wrong_key = Key::createNewRandomKey(); + try { + Crypto::decrypt($ciphertext, $wrong_key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Ciphertext too small. + $key = Key::createNewRandomKey(); + $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); + try { + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + private static function HKDFTestVector() + { + // HKDF test vectors from RFC 5869 + + // Test Case 1 + $ikm = \str_repeat("\x0b", 22); + $salt = Encoding::hexToBin('000102030405060708090a0b0c'); + $info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9'); + $length = 42; + $okm = Encoding::hexToBin( + '3cb25f25faacd57a90434f64d0362f2a' . + '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' . + '34007208d5b887185865' + ); + $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); + Core::ensureTrue($computed_okm === $okm); + + // Test Case 7 + $ikm = \str_repeat("\x0c", 22); + $length = 42; + $okm = Encoding::hexToBin( + '2c91117204d745f3500d636a62f64f0a' . + 'b3bae548aa53d423b0d1f27ebba6f5e5' . + '673a081d70cce7acfc48' + ); + $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); + Core::ensureTrue($computed_okm === $okm); + } + + private static function HMACTestVector() + { + // HMAC test vector From RFC 4231 (Test Case 1) + $key = \str_repeat("\x0b", 20); + $data = 'Hi There'; + $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; + Core::ensureTrue( + \hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) === $correct + ); + } + + private static function AESTestVector() + { + // AES CTR mode test vector from NIST SP 800-38A + $key = Encoding::hexToBin( + '603deb1015ca71be2b73aef0857d7781' . + '1f352c073b6108d72d9810a30914dff4' + ); + $iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); + $plaintext = Encoding::hexToBin( + '6bc1bee22e409f96e93d7e117393172a' . + 'ae2d8a571e03ac9c9eb76fac45af8e51' . + '30c81c46a35ce411e5fbc1191a0a52ef' . + 'f69f2445df4f9b17ad2b417be66c3710' + ); + $ciphertext = Encoding::hexToBin( + '601ec313775789a5b7a7f504bbf3d228' . + 'f443e3ca4d62b59aca84e990cacaf5c5' . + '2b0930daa23de94ce87017ba2d84988d' . + 'dfc9c58db67aada613c2dd08457941a6' + ); + + $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); + Core::ensureTrue($computed_ciphertext === $ciphertext); + + $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); + Core::ensureTrue($computed_plaintext === $plaintext); + } + } +} + +namespace Defuse\Crypto\Exception +{ + class CryptoException extends \Exception + { + } + + class WrongKeyOrModifiedCiphertextException extends \Defuse\Crypto\Exception\CryptoException + { + } +} + +namespace ParagonIE\ConstantTime +{ + interface EncoderInterface + { + public static function encode(string $binString): string; + public static function decode(string $encodedString, bool $strictPadding = false): string; + } + + abstract class Binary + { + public static function safeStrlen(string $str): int + { + if (\function_exists('mb_strlen')) { + return (int) \mb_strlen($str, '8bit'); + } else { + return \strlen($str); + } + } + + public static function safeSubstr( + string $str, + int $start = 0, + $length = null + ): string { + if ($length === 0) { + return ''; + } + if (\function_exists('mb_substr')) { + return \mb_substr($str, $start, $length, '8bit'); + } + // Unlike mb_substr(), substr() doesn't accept NULL for length + if ($length !== null) { + return \substr($str, $start, $length); + } else { + return \substr($str, $start); + } + } + } + + abstract class Base64 implements EncoderInterface + { + public static function encode(string $src): string + { + return static::doEncode($src, true); + } + + protected static function doEncode(string $src, bool $pad = true): string + { + $dest = ''; + $srcLen = Binary::safeStrlen($src); + // Main loop (no padding): + for ($i = 0; $i + 3 <= $srcLen; $i += 3) { + /** @var array+ * 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'
+
+ $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 = '03/08/2019';
+
+ $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=plural(catalog::getTypeByNum($type))?>
+ $cat_data) { ?> + + +Showing =number_format($offset+1)?> - =number_format($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 `