diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php deleted file mode 100644 index b4f75a9ee5..0000000000 --- a/app/controllers/api/avatars.php +++ /dev/null @@ -1,1495 +0,0 @@ -crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); -}; - -$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) { - try { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - $sessions = $user->getAttribute('sessions', []); - - $gitHubSession = null; - foreach ($sessions as $session) { - if ($session->getAttribute('provider', '') === 'github') { - $gitHubSession = $session; - break; - } - } - - if (empty($gitHubSession)) { - throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.'); - } - - $provider = $gitHubSession->getAttribute('provider', ''); - $accessToken = $gitHubSession->getAttribute('providerAccessToken'); - $accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry'); - $refreshToken = $gitHubSession->getAttribute('providerRefreshToken'); - - $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; - - $oAuthProviders = Config::getParam('oAuthProviders'); - $className = $oAuthProviders[$provider]['class']; - if (!\class_exists($className)) { - throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); - } - - $oauth2 = new $className($appId, $appSecret, '', [], []); - - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - try { - $oauth2->refreshTokens($refreshToken); - - $accessToken = $oauth2->getAccessToken(''); - $refreshToken = $oauth2->getRefreshToken(''); - - $verificationId = $oauth2->getUserID($accessToken); - - if (empty($verificationId)) { - throw new \Exception("Locked tokens."); // Race codition, handeled in catch - } - - $gitHubSession - ->setAttribute('providerAccessToken', $accessToken) - ->setAttribute('providerRefreshToken', $refreshToken) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - - Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession)); - - $dbForProject->purgeCachedDocument('users', $user->getId()); - } catch (Throwable $err) { - $index = 0; - do { - $previousAccessToken = $gitHubSession->getAttribute('providerAccessToken'); - - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - $sessions = $user->getAttribute('sessions', []); - - $gitHubSession = new Document(); - foreach ($sessions as $session) { - if ($session->getAttribute('provider', '') === 'github') { - $gitHubSession = $session; - break; - } - } - - $accessToken = $gitHubSession->getAttribute('providerAccessToken'); - - if ($accessToken !== $previousAccessToken) { - break; - } - - $index++; - \usleep(500000); - } while ($index < 10); - } - } - - $oauth2 = new $className($appId, $appSecret, '', [], []); - $githubUser = $oauth2->getUserSlug($accessToken); - $githubId = $oauth2->getUserID($accessToken); - - return [ - 'name' => $githubUser, - 'id' => $githubId - ]; - } catch (Exception $error) { - return []; - } -}; - -App::get('/v1/avatars/credit-cards/:code') - ->desc('Get credit card icon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/credit-card') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getCreditCard', - description: '/docs/references/avatars/get-credit-card.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->inject('response') - ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response)); - -App::get('/v1/avatars/browsers/:code') - ->desc('Get browser icon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/browser') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getBrowser', - description: '/docs/references/avatars/get-browser.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->inject('response') - ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response)); - -App::get('/v1/avatars/flags/:code') - ->desc('Get country flag') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/flag') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getFlag', - description: '/docs/references/avatars/get-flag.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.') - ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) - ->inject('response') - ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response)); - -App::get('/v1/avatars/image') - ->desc('Get image from URL') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/image') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getImage', - description: '/docs/references/avatars/get-image.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE - )) - ->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.') - ->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true) - ->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true) - ->inject('response') - ->action(function (string $url, int $width, int $height, Response $response) { - - $quality = 80; - $output = 'png'; - $type = 'png'; - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(false) - ->fetch($url); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - if ($res->getStatusCode() !== 200) { - throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND); - } - - try { - $image = new Image($res->getBody()); - } catch (\Throwable $exception) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image'); - } - - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); - }); - -App::get('/v1/avatars/favicon') - ->desc('Get favicon') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resource', 'avatar/favicon') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getFavicon', - description: '/docs/references/avatars/get-favicon.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE - )) - ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.') - ->inject('response') - ->action(function (string $url, Response $response) { - - $width = 56; - $height = 56; - $quality = 80; - $output = 'png'; - $type = 'png'; - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(true) - ->setMaxRedirects(5) - ->setUserAgent(\sprintf( - APP_USERAGENT, - System::getEnv('_APP_VERSION', 'UNKNOWN'), - System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)) - )) - ->fetch($url); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $doc = new DOMDocument(); - $doc->strictErrorChecking = false; - @$doc->loadHTML($res->getBody()); - - $links = $doc->getElementsByTagName('link') ?? []; - $outputHref = ''; - $outputExt = ''; - $space = 0; - - foreach ($links as $link) { /* @var $link DOMElement */ - $href = $link->getAttribute('href'); - $rel = $link->getAttribute('rel'); - $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - - switch (\strtolower($rel)) { - case 'icon': - case 'shortcut icon': - //case 'apple-touch-icon': - $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); - - switch ($ext) { - case 'svg': - // SVG icons are prioritized by assigning the maximum possible value. - $space = PHP_INT_MAX; - $outputHref = $absolute; - $outputExt = $ext; - break; - case 'ico': - case 'png': - case 'jpg': - case 'jpeg': - $size = \explode('x', \strtolower($sizes)); - - $sizeWidth = (int) ($size[0] ?? 0); - $sizeHeight = (int) ($size[1] ?? 0); - - if (($sizeWidth * $sizeHeight) >= $space) { - $space = $sizeWidth * $sizeHeight; - $outputHref = $absolute; - $outputExt = $ext; - } - - break; - } - - break; - } - } - - if (empty($outputHref) || empty($outputExt)) { - $default = \parse_url($url); - - $outputHref = $default['scheme'] . '://' . $default['host'] . '/favicon.ico'; - $outputExt = 'ico'; - } - - $domain = new Domain(\parse_url($outputHref, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - try { - $res = $client - ->setAllowRedirects(true) - ->setMaxRedirects(5) - ->fetch($outputHref); - } catch (\Throwable) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - if ($res->getStatusCode() !== 200) { - throw new Exception(Exception::AVATAR_ICON_NOT_FOUND); - } - - $data = $res->getBody(); - - if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - if ( - empty($data) || - stripos($data, 'addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/x-icon') - ->file($data); - return; - } - - if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files - $sanitizer = new SvgSanitizer(); - $sanitizer->minify(true); - $cleanSvg = $sanitizer->sanitize($data); - if ($cleanSvg === false) { - throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED); - } - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/svg+xml') - ->file($cleanSvg); - return; - } - - $image = new Image($data); - $image->crop((int) $width, (int) $height); - $output = (empty($output)) ? $type : $output; - $data = $image->output($output, $quality); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType('image/png') - ->file($data); - unset($image); - }); - -App::get('/v1/avatars/qr') - ->desc('Get QR code') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getQR', - description: '/docs/references/avatars/get-qr.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('text', '', new Text(512), 'Plain text to be converted to QR code image.') - ->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true) - ->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) - ->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true) - ->inject('response') - ->action(function (string $text, int $size, int $margin, bool $download, Response $response) { - - $download = ($download === '1' || $download === 'true' || $download === 1 || $download === true); - $options = new QROptions([ - 'addQuietzone' => true, - 'quietzoneSize' => $margin, - 'outputType' => QRCode::OUTPUT_IMAGICK, - 'scale' => 15, - ]); - - $qrcode = new QRCode($options); - - if ($download) { - $response->addHeader('Content-Disposition', 'attachment; filename="qr.png"'); - } - - $image = new Image($qrcode->render($text)); - $image->crop((int) $size, (int) $size); - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->send($image->output('png', 90)); - }); - -App::get('/v1/avatars/initials') - ->desc('Get user initials') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache.resource', 'avatar/initials') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getInitials', - description: '/docs/references/avatars/get-initials.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('name', '', new Text(128), 'Full Name. When empty, current user name or email will be used. Max length: 128 chars.', true) - ->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('background', '', new HexColor(), 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) - ->inject('response') - ->inject('user') - ->action(function (string $name, int $width, int $height, string $background, Response $response, Document $user) { - - $themes = [ - ['background' => '#FD366E'], // Default (Pink) - ['background' => '#FE9567'], // Orange - ['background' => '#7C67FE'], // Purple - ['background' => '#68A3FE'], // Blue - ['background' => '#85DBD8'], // Mint - ]; - - $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); - $words = \explode(' ', \strtoupper($name)); - // if there is no space, try to split by `_` underscore - $words = (count($words) == 1) ? \explode('_', \strtoupper($name)) : $words; - - $initials = ''; - $code = 0; - - foreach ($words as $key => $w) { - if (ctype_alnum($w[0] ?? '')) { - $initials .= $w[0]; - $code += ord($w[0]); - - if ($key == 1) { - break; - } - } - } - - $rand = \substr($code, -1); - - // Wrap rand value to avoid out of range - $rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand; - - $background = (!empty($background)) ? '#' . $background : $themes[$rand]['background']; - - $image = new \Imagick(); - $punch = new \Imagick(); - $draw = new \ImagickDraw(); - $fontSize = \min($width, $height) / 2; - - $punch->newImage($width, $height, 'transparent'); - - $draw->setFont(__DIR__ . "/../../assets/fonts/inter-v8-latin-regular.woff2"); - $image->setFont(__DIR__ . "/../../assets/fonts/inter-v8-latin-regular.woff2"); - - $draw->setFillColor(new ImagickPixel('black')); - $draw->setFontSize($fontSize); - - $draw->setTextAlignment(\Imagick::ALIGN_CENTER); - $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); - - $punch->drawImage($draw); - $punch->negateImage(true, Imagick::CHANNEL_ALPHA); - - $image->newImage($width, $height, $background); - $image->setImageFormat("png"); - $image->compositeImage($punch, Imagick::COMPOSITE_COPYOPACITY, 0, 0); - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($image->getImageBlob()); - }); - -App::get('/v1/avatars/screenshots') - ->desc('Get webpage screenshot') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED) - ->label('abuse-limit', 60) - ->label('cache', true) - ->label('cache.resourceType', 'avatar/screenshot') - ->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}') - ->label('sdk', new Method( - namespace: 'avatars', - group: null, - name: 'getScreenshot', - description: '/docs/references/avatars/get-screenshot.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT], - type: MethodType::LOCATION, - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::IMAGE_PNG - )) - ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com') - ->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}') - ->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920') - ->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080') - ->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2') - ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark') - ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15') - ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true') - ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US') - ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york') - ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749') - ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194') - ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100') - ->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true') - ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]') - ->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3') - ->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800') - ->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600') - ->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85') - ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg') - ->inject('response') - ->inject('queueForStatsUsage') - ->action(function (string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) { - - if (!\extension_loaded('imagick')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); - } - - $domain = new Domain(\parse_url($url, PHP_URL_HOST)); - - if (!$domain->isKnown()) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED); - } - - $client = new Client(); - $client->setTimeout(30 * 1000); // 30 seconds - $client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON); - - // Convert indexed array to empty array (should not happen due to Assoc validator) - if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) { - $headers = []; - } - - // Create a new object to ensure proper JSON serialization - $headersObject = new \stdClass(); - foreach ($headers as $key => $value) { - $headersObject->$key = $value; - } - - // Create the config with headers as an object - // The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale - $config = [ - 'url' => $url, - 'theme' => $theme, - 'headers' => $headersObject, - 'sleep' => $sleep * 1000, // Convert seconds to milliseconds - 'waitUntil' => 'load', - 'viewport' => [ - 'width' => $viewportWidth, - 'height' => $viewportHeight - ] - ]; - - // Add scale if not default - if ($scale != 1) { - $config['deviceScaleFactor'] = $scale; - } - - // Add optional parameters that were set, preserving arrays as arrays - if (!empty($userAgent)) { - $config['userAgent'] = $userAgent; - } - - if ($fullpage) { - $config['fullPage'] = true; - } - - if (!empty($locale)) { - $config['locale'] = $locale; - } - - if (!empty($timezone)) { - $config['timezoneId'] = $timezone; - } - - // Add geolocation if any coordinates are provided - if ($latitude != 0 || $longitude != 0) { - $config['geolocation'] = [ - 'latitude' => $latitude, - 'longitude' => $longitude, - 'accuracy' => $accuracy - ]; - } - - if ($touch) { - $config['hasTouch'] = true; - } - - // Add permissions if provided (preserve as array) - if (!empty($permissions)) { - $config['permissions'] = $permissions; // Keep as array - } - - try { - $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); - - $fetchResponse = $client->fetch( - url: $browserEndpoint . '/screenshots', - method: 'POST', - body: $config - ); - - if ($fetchResponse->getStatusCode() >= 400) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody()); - } - - $screenshot = $fetchResponse->getBody(); - - if (empty($screenshot)) { - throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated'); - } - - // Determine if image processing is needed - $needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output); - - if ($needsProcessing) { - // Process image with cropping, quality adjustment, or format conversion - $image = new Image($screenshot); - - $image->crop($width, $height); - - $output = $output ?: 'png'; // Default to PNG if not specified - $resizedScreenshot = $image->output($output, $quality); - unset($image); - } else { - // Return original screenshot without processing - $resizedScreenshot = $screenshot; - $output = 'png'; // Screenshots are typically PNG by default - } - - // Set content type based on output format - $outputs = Config::getParam('storage-outputs'); - $contentType = $outputs[$output] ?? $outputs['png']; - - $queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1); - - $response - ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days - ->setContentType($contentType) - ->file($resizedScreenshot); - - - } catch (\Throwable $th) { - throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage()); - } - }); - -App::get('/v1/cards/cloud') - ->desc('Get front Of Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud') - ->label('cache.resource', 'card/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) - ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $name = $user->getAttribute('name', 'Anonymous'); - $email = $user->getAttribute('email', ''); - $createdAt = new \DateTime($user->getCreatedAt()); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubName = $gitHub['name'] ?? ''; - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; - - if ($isHero) { - $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); - } elseif ($isEmployee) { - $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); - } - - if (!$isEmployee && !empty($githubName)) { - $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); - if (!empty($employeeGitHub)) { - $isEmployee = true; - $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; - $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); - } - } - - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; - $createdAt = new \DateTime('now'); - $githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian'); - $isHero = $mock === 'hero'; - $isContributor = $mock === 'contributor'; - $isEmployee = \str_starts_with($mock, 'employee'); - $employeeNumber = match ($mock) { - 'employee' => '1', - 'employee-2digit' => '18', - default => '' - }; - - $isPlatinum = $mock === 'platinum'; - } - - if ($isEmployee) { - $isContributor = false; - $isHero = false; - } - - if ($isHero) { - $isContributor = false; - $isEmployee = false; - } - - if ($isContributor) { - $isHero = false; - $isEmployee = false; - } - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $isGolden ? false : $isPlatinum; - $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); - - $imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png'); - - $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); - - if ($isEmployee) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/employee.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFADF')); - $text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48); - $text->setFontWeight(700); - $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); - - $hashtag = new \ImagickDraw(); - $hashtag->setTextAlignment(Imagick::ALIGN_CENTER); - $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); - $hashtag->setFontSize(28); - $hashtag->setFontWeight(700); - $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); - - $startX = 898; - $totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth']; - - $hashtagX = ($metricsHashtag['textWidth'] / 2); - $textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2); - - $hashtagX -= $totalWidth / 2; - $textX -= $totalWidth / 2; - - $hashtagX += $startX; - $textX += $startX; - - $baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#'); - $baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber); - } - - if ($isContributor) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/contributor.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); - } - - if ($isHero) { - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/hero.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34); - } - - setlocale(LC_ALL, "en_US.utf8"); - // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); - // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); - // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - - if (\strlen($name) > 32) { - $name = \substr($name, 0, 32); - } - - if (\strlen($name) <= 23) { - $text->setFontSize(80); - $scalingDown = false; - } else { - $text->setFontSize(54); - $scalingDown = true; - } - $text->setFontWeight(700); - $baseImage->annotateImage($text, 512, 477, 0, $name); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-SemiBold.ttf'); - $text->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); - $text->setFontSize(27); - $text->setFontWeight(600); - $text->setTextKerning(1.08); - $baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince)); - - if (!empty($githubName)) { - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - $text->setFontSize($scalingDown ? 28 : 32); - $text->setFontWeight(400); - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName); - - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $precisionFix = 5; - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix)); - } - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - }); - -App::get('/v1/cards/cloud-back') - ->desc('Get back Of Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud-back') - ->label('cache.resource', 'card-back/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true) - ->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $userId = $user->getId(); - $email = $user->getAttribute('email', ''); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $userId = '63e0bcf3c3eb803ba530'; - - $isGolden = $mock === 'golden'; - $isPlatinum = $mock === 'platinum'; - } - - $userId = 'UID ' . $userId; - - $isPlatinum = $isGolden ? false : $isPlatinum; - - $imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png'); - - $baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath); - - setlocale(LC_ALL, "en_US.utf8"); - // $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_CENTER); - $text->setFont(__DIR__ . '/../../../public/fonts/SourceCodePro-Regular.ttf'); - $text->setFillColor(new \ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0'))); - $text->setFontSize(28); - $text->setFontWeight(400); - $baseImage->annotateImage($text, 512, 596, 0, $userId); - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - }); - -App::get('/v1/cards/cloud-og') - ->desc('Get OG image From Cloud Card') - ->groups(['api', 'avatars']) - ->label('scope', 'avatars.read') - ->label('cache', true) - ->label('cache.resourceType', 'cards/cloud-og') - ->label('cache.resource', 'card-og/{request.userId}') - ->label('docs', false) - ->label('origin', '*') - ->param('userId', '', new UID(), 'User ID.', true) - ->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true) - ->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true) - ->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true) - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('response') - ->inject('heroes') - ->inject('contributors') - ->inject('employees') - ->inject('logger') - ->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) { - $user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId)); - - if ($user->isEmpty() && empty($mock)) { - throw new Exception(Exception::USER_NOT_FOUND); - } - - if (!$mock) { - $sequence = $user->getSequence(); - $bgVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); - $cardVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3'); - - $name = $user->getAttribute('name', 'Anonymous'); - $email = $user->getAttribute('email', ''); - $createdAt = new \DateTime($user->getCreatedAt()); - - $gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger); - $githubName = $gitHub['name'] ?? ''; - $githubId = $gitHub['id'] ?? ''; - - $isHero = \array_key_exists($email, $heroes); - $isContributor = \in_array($githubId, $contributors); - $isEmployee = \array_key_exists($email, $employees); - $employeeNumber = $isEmployee ? $employees[$email]['spot'] : ''; - - if ($isHero) { - $createdAt = new \DateTime($heroes[$email]['memberSince'] ?? ''); - } elseif ($isEmployee) { - $createdAt = new \DateTime($employees[$email]['memberSince'] ?? ''); - } - - if (!$isEmployee && !empty($githubName)) { - $employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees)); - if (!empty($employeeGitHub)) { - $isEmployee = true; - $employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : ''; - $createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? ''); - } - } - - $isPlatinum = $user->getSequence() % 100 === 0; - } else { - $bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1'); - $cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1'); - $name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian'; - $createdAt = new \DateTime('now'); - $githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian'); - $isHero = \str_starts_with($mock, 'hero'); - $isContributor = \str_starts_with($mock, 'contributor'); - $isEmployee = \str_starts_with($mock, 'employee'); - $employeeNumber = match ($mock) { - 'employee' => '1', - 'employee-right' => '1', - 'employee-middle' => '1', - 'employee-2digit' => '18', - 'employee-2digit-right' => '18', - 'employee-2digit-middle' => '18', - default => '' - }; - - $isPlatinum = \str_starts_with($mock, 'platinum'); - } - - if ($isEmployee) { - $isContributor = false; - $isHero = false; - } - - if ($isHero) { - $isContributor = false; - $isEmployee = false; - } - - if ($isContributor) { - $isHero = false; - $isEmployee = false; - } - - $isGolden = $isEmployee || $isHero || $isContributor; - $isPlatinum = $isGolden ? false : $isPlatinum; - $memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o')); - - $baseImage = new \Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-background{$bgVariation}.png"); - - $cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : ''); - - $image = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png"); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2); - - $imageLogo = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/og-background-logo.png'); - $imageShadow = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-shadow{$cardType}.png"); - if ($cardVariation === '1') { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700); - } elseif ($cardVariation === '2') { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710); - } else { - $baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32); - $baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710); - } - - if ($isEmployee) { - $file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - $hashtag = new \ImagickDraw(); - $hashtag->setTextAlignment(Imagick::ALIGN_LEFT); - $hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $hashtag->setFillColor(new \ImagickPixel('#FFFADF')); - $hashtag->setFontSize(20); - $hashtag->setFontWeight(700); - - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_LEFT); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFADF')); - $text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28); - $text->setFontWeight(700); - - if ($cardVariation === '3') { - $hashtag->setFontSize(16); - $text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26); - - $hashtag->skewY(20); - $hashtag->skewX(20); - $text->skewY(20); - $text->skewX(20); - } - - $metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#'); - $metricsText = $baseImage->queryFontMetrics($text, $employeeNumber); - - $group = new Imagick(); - $groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth']; - - if ($cardVariation === '1') { - $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); - $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); - $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); - - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - - $group->rotateImage(new ImagickPixel('#00000000'), -22); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247); - } - } elseif ($cardVariation === '2') { - $group->newImage($groupWidth, $metricsText['textHeight'], '#00000000'); - $group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#'); - $group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber); - - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - - $group->rotateImage(new ImagickPixel('#00000000'), 32); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470); - } - } else { - $group->newImage(300, 300, '#00000000'); - - $hashtag->annotation(0, $metricsText['textHeight'], '#'); - $text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber); - - $group->drawImage($hashtag); - $group->drawImage($text); - - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - - if (\strlen($employeeNumber) <= 1) { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317); - } else { - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322); - } - } - } - - if ($isContributor) { - $file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - if ($cardVariation === '1') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - } elseif ($cardVariation === '2') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - } else { - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - } - } - - if ($isHero) { - $file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png'; - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file); - $image->setGravity(Imagick::GRAVITY_CENTER); - - if ($cardVariation === '1') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), -20); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203); - } elseif ($cardVariation === '2') { - $image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1); - $image->rotateImage(new ImagickPixel('#00000000'), 30); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425); - } else { - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293); - } - } - - setlocale(LC_ALL, "en_US.utf8"); - // $name = \iconv("utf-8", "ascii//TRANSLIT", $name); - // $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince); - // $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName); - - $textName = new \ImagickDraw(); - $textName->setTextAlignment(Imagick::ALIGN_CENTER); - $textName->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf'); - $textName->setFillColor(new \ImagickPixel('#FFFFFF')); - - if (\strlen($name) > 32) { - $name = \substr($name, 0, 32); - } - - if ($cardVariation === '1') { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(54); - } else { - $scalingDown = true; - $textName->setFontSize(36); - } - } elseif ($cardVariation === '2') { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(50); - } else { - $scalingDown = true; - $textName->setFontSize(34); - } - } else { - if (\strlen($name) <= 23) { - $scalingDown = false; - $textName->setFontSize(44); - } else { - $scalingDown = true; - $textName->setFontSize(32); - } - } - - $textName->setFontWeight(700); - - $textMember = new \ImagickDraw(); - $textMember->setTextAlignment(Imagick::ALIGN_CENTER); - $textMember->setFont(__DIR__ . '/../../../public/fonts/Inter-Medium.ttf'); - $textMember->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC')); - $textMember->setFontWeight(500); - $textMember->setTextKerning(1.12); - - if ($cardVariation === '1') { - $textMember->setFontSize(21); - - $baseImage->annotateImage($textName, 550, 600, -22, $name); - $baseImage->annotateImage($textMember, 585, 635, -22, $memberSince); - } elseif ($cardVariation === '2') { - $textMember->setFontSize(20); - - $baseImage->annotateImage($textName, 435, 590, 31.37, $name); - $baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince); - } else { - $textMember->setFontSize(16); - - $textName->skewY(20); - $textName->skewX(20); - $textName->annotation(320, 700, $name); - - $textMember->skewY(20); - $textMember->skewX(20); - $textMember->annotation(330, 735, $memberSince); - - $baseImage->drawImage($textName); - $baseImage->drawImage($textMember); - } - - if (!empty($githubName)) { - $text = new \ImagickDraw(); - $text->setTextAlignment(Imagick::ALIGN_LEFT); - $text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf'); - $text->setFillColor(new \ImagickPixel('#FFFFFF')); - $text->setFontSize($scalingDown ? 16 : 20); - $text->setFontWeight(400); - - if ($cardVariation === '1') { - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $group = new Imagick(); - $groupWidth = $metrics['textWidth'] + 32 + 4; - $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); - $precisionFix = -1; - - $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); - $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); - - $group->rotateImage(new ImagickPixel('#00000000'), -22); - $x = 510 - $group->getImageWidth() / 2; - $y = 530 - $group->getImageHeight() / 2; - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); - } elseif ($cardVariation === '2') { - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $group = new Imagick(); - $groupWidth = $metrics['textWidth'] + 32 + 4; - $group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000'); - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); - $precisionFix = -1; - - $group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0); - $group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName); - - $group->rotateImage(new ImagickPixel('#00000000'), 31.11); - $x = 485 - $group->getImageWidth() / 2; - $y = 530 - $group->getImageHeight() / 2; - $baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y); - } else { - $text->skewY(20); - $text->skewX(20); - $text->setTextAlignment(\Imagick::ALIGN_CENTER); - - $text->annotation(320 + 15 + 2, 640, $githubName); - $metrics = $baseImage->queryFontMetrics($text, $githubName); - - $image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github-skew.png'); - $image->setGravity(Imagick::GRAVITY_CENTER); - $baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3); - - $baseImage->drawImage($text); - } - } - - if (!empty($width) || !empty($height)) { - $baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1); - } - - $response - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->setContentType('image/png') - ->file($baseImage->getImageBlob()); - });