Initial import

This commit is contained in:
flash 2022-07-03 22:41:44 +00:00
commit b1311e336c
9 changed files with 1072 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/.debug
/config.php

49
lib/database/database.php Normal file
View file

@ -0,0 +1,49 @@
<?php
namespace Database;
use PDO;
class Database {
public $pdo;
private $stmts = [];
public function __construct(string $dsn, string $username = '', string $password = '', array $options = []) {
$this->pdo = new PDO($dsn, $username, $password, $options);
}
public function getPDO(): PDO {
return $this->pdo;
}
public function queries(): int {
return (int)$this->query('SHOW SESSION STATUS LIKE "Questions"')->fetchColumn(1);
}
public function exec(string $stmt): int {
return $this->pdo->exec($stmt);
}
public function prepare(string $stmt, array $options = []): DatabaseStatement {
$encodedOptions = serialize($options);
if(empty($this->stmts[$stmt][$encodedOptions])) {
$this->stmts[$stmt][$encodedOptions] = $this->pdo->prepare($stmt, $options);
}
return new DatabaseStatement($this->stmts[$stmt][$encodedOptions], $this->pdo, false);
}
public function query(string $stmt, ?int $fetchMode = null, ...$args): DatabaseStatement {
if($fetchMode === null) {
$pdoStmt = $this->pdo->query($stmt);
} else {
$pdoStmt = $this->pdo->query($stmt, $fetchMode, ...$args);
}
return new DatabaseStatement($pdoStmt, $this->pdo, true);
}
public function lastId(): int {
return $this->pdo->lastInsertId();
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Database;
use PDO;
use PDOStatement;
class DatabaseStatement {
public $pdo;
public $stmt;
private $isQuery;
public function __construct(PDOStatement $stmt, PDO $pdo, bool $isQuery) {
$this->stmt = $stmt;
$this->pdo = $pdo;
$this->isQuery = $isQuery;
}
public function bind($param, $value, int $dataType = PDO::PARAM_STR): DatabaseStatement {
$this->stmt->bindValue($param, $value, $dataType);
return $this;
}
public function execute(array $params = []): bool {
return count($params) ? $this->stmt->execute($params) : $this->stmt->execute();
}
public function executeGetId(array $params = []): int {
return $this->execute($params) ? $this->pdo->lastInsertId() : 0;
}
public function fetch($default = []) {
$out = $this->isQuery || $this->execute() ? $this->stmt->fetch(PDO::FETCH_ASSOC) : false;
return $out ? $out : $default;
}
public function fetchAll($default = []) {
$out = $this->isQuery || $this->execute() ? $this->stmt->fetchAll(PDO::FETCH_ASSOC) : false;
return $out ? $out : $default;
}
public function fetchColumn(int $num = 0, $default = null) {
$out = $this->isQuery || $this->execute() ? $this->stmt->fetchColumn($num) : false;
return $out ? $out : $default;
}
public function fetchObject(string $className = 'stdClass', ?array $args = null, $default = null) {
$out = false;
if($this->isQuery || $this->execute()) {
$out = $args === null ? $this->stmt->fetchObject($className) : $this->stmt->fetchObject($className, $args);
}
return $out !== false ? $out : $default;
}
public function fetchObjects(string $className = 'stdClass', ?array $args = null): array {
$objects = [];
if($this->isQuery || $this->execute()) {
while(($object = ($args === null ? $this->stmt->fetchObject($className) : $this->stmt->fetchObject($className, $args))) !== false) {
$objects[] = $object;
}
}
return $objects;
}
}

24
lib/db.php Normal file
View file

@ -0,0 +1,24 @@
<?php
use Database\Database;
final class DB {
private static $instance;
public const ATTRS = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET SESSION time_zone = \'+00:00\''
. ', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'',
];
public static function init(...$args) {
self::$instance = new Database(...$args);
}
public static function __callStatic(string $name, array $args) {
return self::$instance->{$name}(...$args);
}
}

107
lib/mediatype.php Normal file
View file

@ -0,0 +1,107 @@
<?php
class MediaType implements JsonSerializable {
private $type = '';
private $subtype = '';
private $suffix = '';
private $params = [];
public function __construct(string $mediaType) {
if(preg_match('#^([A-Za-z0-9\!\#\$%&\'\*\+\.\-_\{\}\|]+)/([A-Za-z0-9\!\#\$%&\'\*\+\.\-_\{\}\|]+)(?: ?; ?([A-Za-z0-9\!\#\$%&\'\*\+\.\-_\{\}\|\=; ]+))?$#', $mediaType, $matches) !== 1)
throw new InvalidArgumentException('Invalid media type supplied.');
$this->type = $matches[1];
$subTypeSplit = explode('+', $matches[2], 2);
$this->subtype = $subTypeSplit[0];
if(isset($subTypeSplit[1]))
$this->suffix = $subTypeSplit[1];
if(isset($matches[3])) {
$params = explode(';', $matches[3]);
foreach($params as $param) {
$parts = explode('=', trim($param), 2);
if(!isset($parts[1]))
continue;
$this->params[$parts[0]] = $parts[1];
}
}
}
public function getType(): string {
return $this->type;
}
public function getSubtype(): string {
return $this->subtype;
}
public function getSuffix(): string {
return $this->subtype;
}
public function getParams(): array {
return $this->params;
}
public function getParam(string $name, int $filter = FILTER_DEFAULT, $options = null) {
if(!isset($this->params[$name]))
return null;
return filter_var($this->params[$name], $filter, $options);
}
public function getCharset(): string {
return $this->getParam('charset', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? 'utf-8';
}
public function getQuality(): float {
return max(min(round($this->getParam('q', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION) ?? 1, 2), 1), 0);
}
public function match($other): bool {
if(is_string($other))
return $this->matchPattern($other);
if($other instanceof self)
return $this->matchType($other);
return false;
}
public function matchPattern(string $pattern): bool {
try {
$mediaType = new static($pattern);
} catch(InvalidArgumentException $ex) {
return false;
}
return $this->matchType($mediaType);
}
public function matchType(MediaType $other): bool {
return ($other->getType() === '*' && $other->getSubtype() === '*')
|| (
($other->getType() === $this->getType())
&& ($other->getSubtype() === '*' || $other->getSubtype() === $this->getSubtype())
);
}
public function __toString() {
$string = $this->type . '/';
$string .= $this->subtype;
if(!empty($this->suffix))
$string .= '+' . $this->suffix;
if(!empty($this->params))
foreach($this->params as $key => $value) {
$string .= ';';
if(!empty($key))
$string .= $key . '=';
$string .= $value;
}
return $string;
}
public function jsonSerialize(): stdClass {
$obj = new stdClass;
$obj->string = (string)$this;
$obj->type = $this->type;
$obj->subtype = $this->subtype;
if(!empty($this->suffix))
$obj->suffix = $this->suffix;
if(!empty($this->params))
$obj->params = $this->params;
return $obj;
}
}

55
lib/stopwatch.php Normal file
View file

@ -0,0 +1,55 @@
<?php
final class Stopwatch {
private $startTime = 0;
private $stopTime = 0;
private $laps = [];
private static $instance = null;
public function __call(string $name, array $args) {
if($name[0] === '_')
return null;
return $this->{'_' . $name}(...$args);
}
public static function __callStatic(string $name, array $args) {
if($name[0] === '_')
return null;
if(self::$instance === null)
self::$instance = new static;
return self::$instance->{'_' . $name}(...$args);
}
private static function time() {
return microtime(true);
}
public function _start(): void {
$this->startTime = self::time();
}
public function _lap(string $text): void {
$this->laps[$text] = self::time();
}
public function _stop(): void {
$this->stopTime = self::time();
}
public function _reset(): void {
$this->laps = [];
$this->startTime = 0;
$this->stopTime = 0;
}
public function _elapsed(): float {
return $this->stopTime - $this->startTime;
}
public function _laps(): array {
$laps = [];
foreach($this->laps as $name => $time)
$laps[$name] = $time - $this->startTime;
return $laps;
}
}

182
lib/uri.php Normal file
View file

@ -0,0 +1,182 @@
<?php
class Uri implements JsonSerializable {
private $scheme = '';
private $user = '';
private $password = '';
private $host = '';
private $port = null;
private $path = '';
private $query = '';
private $fragment = '';
private $originalString = '';
public function __construct(string $uriString = '') {
$this->originalString = $uriString;
if(!empty($uriString)) {
$uri = parse_url($uriString);
if($uri === false)
throw new InvalidArgumentException('URI cannot be parsed.');
$this->setScheme($uri['scheme'] ?? '');
$this->setUserInfo($uri['user'] ?? '', $uri['pass'] ?? null);
$this->setHost($uri['host'] ?? '');
$this->setPort($uri['port'] ?? null);
$this->setPath($uri['path'] ?? '');
$this->setQuery($uri['query'] ?? '');
$this->setFragment($uri['fragment'] ?? '');
}
}
public function getHash(): string {
return hash('sha256', (string)$this);
}
public function jsonSerialize(): stdClass {
$obj = new stdClass;
$obj->uri = (string)$this;
if($this->scheme !== '')
$obj->scheme = $this->scheme;
if($this->user !== '')
$obj->user = $this->user;
if($this->password !== null)
$obj->password = $this->password;
if($this->host !== '')
$obj->host = $this->host;
if($this->port !== null)
$obj->port = $this->port;
if($this->path !== '')
$obj->path = $this->path;
if($this->query !== '')
$obj->query = $this->query;
if($this->fragment !== '')
$obj->fragment = $this->fragment;
//$obj->hash = $this->getHash();
return $obj;
}
public function getOriginalString(): string {
return $this->originalString;
}
public function getScheme() {
return $this->scheme;
}
public function setScheme(string $scheme): self {
$this->scheme = $scheme;
return $this;
}
public function getAuthority() {
$authority = '';
if(!empty($userInfo = $this->getUserInfo()))
$authority .= $userInfo . '@';
$authority .= $this->getHost();
if(($port = $this->getPort()) !== null)
$authority .= ':' . $port;
return $authority;
}
public function getUserInfo() {
$userInfo = $this->user;
if(!empty($this->password))
$userInfo .= ':' . $this->password;
return $userInfo;
}
public function setUserInfo(string $user, ?string $password = null): self {
$this->user = $user;
$this->password = $password;
return $this;
}
public function getHost() {
return $this->host;
}
public function setHost(string $host): self {
$this->host = $host;
return $this;
}
public function getPort() {
return $this->port;
}
public function setPort(?int $port): self {
if($port !== null && ($port < 1 || $port > 0xFFFF))
throw new InvalidArgumentException('Invalid port.');
$this->port = $port;
return $this;
}
public function getPath() {
return $this->path;
}
public function setPath(string $path): self {
$this->path = $path;
return $this;
}
public function getQuery() {
return $this->query;
}
public function setQuery(string $query): self {
$this->query = $query;
return $this;
}
public function getFragment() {
return $this->fragment;
}
public function setFragment(string $fragment): self {
$this->fragment = $fragment;
return $this;
}
public function __toString() {
$string = '';
if(!empty($scheme = $this->getScheme()))
$string .= $scheme . ':';
$authority = $this->getAuthority();
$hasAuthority = !empty($authority);
if($hasAuthority)
$string .= '//' . $authority;
$path = $this->getPath();
$hasPath = !empty($path);
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
$string .= '/';
elseif(!$hasAuthority && $path[1] === '/')
$path = '/' . trim($path, '/');
$string .= $path;
if(!empty($query = $this->getQuery())) {
$string .= '?';
$queryParts = explode('&', $query);
foreach($queryParts as $queryPart) {
$kvp = explode('=', $queryPart, 2);
$string .= rawurlencode($kvp[0]);
if(isset($kvp[1]))
$string .= '=' . rawurlencode($kvp[1]);
$string .= '&';
}
$string = substr($string, 0, -1);
}
if(!empty($fragment = $this->getFragment()))
$string .= '#' . rawurlencode($fragment);
return $string;
}
}

585
public/index.php Normal file
View file

@ -0,0 +1,585 @@
<?php
define('UIH_VERSION', '20201009');
define('UIH_DEBUG', isset($_GET['_debug']));
define('UIH_CACHE', !UIH_DEBUG && !isset($_GET['_skip']));
define('UIH_INCLUDE_RAW', UIH_DEBUG || isset($_GET['include_raw']));
define('UIH_SEM_NAME', 'U');
define('UIH_SEM_PATH', sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'uiharu');
function uih_eeprom_lookup(stdClass $resp, string $eepromFileId, string $domain = 'flashii'): void {
$resp->type = 'eeprom:file';
$resp->color = '#8559a5';
$resp->eeprom_file_id = $eepromFileId;
$curl = curl_init("https://eeprom.{$domain}.net/uploads/{$resp->eeprom_file_id}.json");
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
],
]);
$eepromResp = curl_exec($curl);
curl_close($curl);
$resp->eeprom_file_info = json_decode($eepromResp);
if(isset($resp->eeprom_file_info->name))
$resp->title = $resp->eeprom_file_info->name;
if(isset($resp->eeprom_file_info->thumb))
$resp->image = $resp->eeprom_file_info->thumb;
$resp->site_name = 'Flashii EEPROM';
}
if(!is_dir(UIH_SEM_PATH))
mkdir(UIH_SEM_PATH, 0777, true);
require_once __DIR__ . '/../config.php';
header('X-Powered-By: Uiharu');
ini_set('display_errors', UIH_DEBUG ? 'on' : 'off');
error_reporting(UIH_DEBUG ? -1 : 0);
set_include_path(realpath(__DIR__ . '/../lib/') . PATH_SEPARATOR . get_include_path());
spl_autoload_extensions('.php');
spl_autoload_register();
DB::init(UIH_PDO_DSN, UIH_PDO_USER, UIH_PDO_PASS, DB::ATTRS);
DB::exec('DELETE FROM `uih_metadata_cache` WHERE `metadata_created` < NOW() - INTERVAL 7 DAY');
$reqMethod = filter_input(INPUT_SERVER, 'REQUEST_METHOD', FILTER_SANITIZE_STRING);
$reqPath = '/' . trim(parse_url(filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_STRING), PHP_URL_PATH), '/');
$reqHead = false;
if($reqMethod == 'HEAD') {
$reqMethod = 'GET';
$reqHead = true;
}
if(!empty($_SERVER['HTTP_ORIGIN'])) {
$originLast12 = substr($_SERVER['HTTP_ORIGIN'], -12, 12);
$originLast10 = substr($_SERVER['HTTP_ORIGIN'], -10, 10);
if($originLast12 !== '/flashii.net' && $originLast12 !== '.flashii.net'
&& $originLast10 !== '/edgii.net' && $originLast10 !== '.edgii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'https://flashii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'http://flashii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'https://edgii.net'
&& $_SERVER['HTTP_ORIGIN'] !== 'http://edgii.net') {
http_response_code(403);
return;
}
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header('Vary: Origin');
}
if($reqMethod === 'OPTIONS') {
http_response_code(204);
//header('Access-Control-Allow-Credentials: true');
//header('Access-Control-Allow-Headers: Authorization');
header('Access-Control-Allow-Methods: OPTIONS, GET, POST');
return;
}
if($reqPath === '/metadata') {
// Allow using POST for ridiculous urls.
if($reqMethod !== 'GET' && $reqMethod !== 'POST') {
http_response_code(405);
return;
}
header('Content-Type: application/json; charset=utf-8');
if($reqHead)
return;
Stopwatch::start();
$resp = new stdClass;
if($reqMethod === 'POST') {
$targetUrl = substr((string)file_get_contents('php://input'), 0, 1000);
} else {
$targetUrl = (string)filter_input(INPUT_GET, 'url');
}
try {
$resp->uri = $parsedUrl = new Uri($targetUrl);
} catch(InvalidArgumentException $ex) {
http_response_code(400);
$resp->error = 'metadata:uri';
echo json_encode($resp);
return;
}
// if no scheme is specified, try https
if($parsedUrl->getScheme() === '')
$parsedUrl = new Uri('https://' . (string)$parsedUrl);
$urlHash = $parsedUrl->getHash();
try {
$semPath = UIH_SEM_PATH . DIRECTORY_SEPARATOR . $urlHash;
if(!is_file($semPath))
touch($semPath);
$ftok = ftok($semPath, UIH_SEM_NAME);
$semaphore = sem_get($ftok, 1);
while(!sem_acquire($semaphore)) usleep(100);
if(UIH_CACHE) {
$loadCache = DB::prepare('SELECT `metadata_resp` AS `resp` FROM `uih_metadata_cache` WHERE `metadata_url` = UNHEX(:hash) AND `metadata_created` > NOW() - INTERVAL 10 MINUTE')
->bind('hash', $urlHash)
->fetchObject();
if(isset($loadCache->resp)) {
$cacheResp = json_decode($loadCache->resp);
if($cacheResp !== null)
$resp = $cacheResp;
}
}
if(empty($resp->type)) {
$urlScheme = strtolower($parsedUrl->getScheme());
$urlHost = strtolower($parsedUrl->getHost());
$urlPath = '/' . trim($parsedUrl->getPath(), '/');
if($urlScheme === 'eeprom') {
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) {
$resp->uri = $parsedUrl = new Uri('https://i.fii.moe/' . $matches[1]);
$continueRaw = true;
uih_eeprom_lookup($resp, $matches[1]);
}
} elseif($urlScheme === 'devrom') {
if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) {
$resp->uri = $parsedUrl = new Uri('https://i.edgii.net/' . $matches[1]);
$continueRaw = true;
uih_eeprom_lookup($resp, $matches[1], 'edgii');
}
} elseif($urlScheme === 'http' || $urlScheme === 'https') {
switch($urlHost) {
case 'i.flashii.net':
case 'i.fii.moe':
$eepromFileId = substr($urlPath, 1);
case 'eeprom.flashii.net':
if(!isset($eepromFileId) && preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $urlPath, $matches))
$eepromFileId = $matches[1];
if(!empty($eepromFileId)) {
$continueRaw = true;
uih_eeprom_lookup($resp, $eepromFileId);
}
break;
case 'i.edgii.net':
$eepromFileId = substr($urlPath, 1);
case 'eeprom.edgii.net':
if(!isset($eepromFileId) && preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $urlPath, $matches))
$eepromFileId = $matches[1];
if(!empty($eepromFileId)) {
$continueRaw = true;
uih_eeprom_lookup($resp, $eepromFileId, 'edgii');
}
break;
case 'twitter.com': case 'www.twitter.com':
case 'm.twitter.com': case 'mobile.twitter.com':
case 'nitter.net': case 'www.nitter.net':
if(preg_match('#^/@?(?:[A-Za-z0-9_]{1,20})/status(?:es)?/([0-9]+)/?$#', $urlPath, $matches)) {
$resp->type = 'twitter:tweet';
$resp->color = '#1da1f2';
$resp->tweet_id = strval($matches[1] ?? '0');
$curl = curl_init("https://api.twitter.com/2/tweets?ids={$resp->tweet_id}&expansions=attachments.media_keys,author_id,entities.mentions.username,referenced_tweets.id,referenced_tweets.id.author_id&media.fields=height,width,media_key,preview_image_url,url,type&tweet.fields=attachments,conversation_id,text,source,possibly_sensitive,created_at&user.fields=id,name,profile_image_url,protected,username,verified");
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . TWITTER_API_TOKEN,
'Accept: application/json',
],
]);
$tweetResp = curl_exec($curl);
curl_close($curl);
$resp->tweet_info = json_decode($tweetResp);
if(isset($resp->tweet_info->includes->users[0]->name))
$resp->title = $resp->tweet_info->includes->users[0]->name;
if(isset($resp->tweet_info->includes->users[0]->profile_image_url))
$resp->image = $resp->tweet_info->includes->users[0]->profile_image_url;
if(isset($resp->tweet_info->data[0]->text))
$resp->description = $resp->tweet_info->data[0]->text;
$resp->site_name = 'Twitter';
break;
}
if(preg_match('#^/@?([A-Za-z0-9_]{1,20})/?$#', $urlPath, $matches)) {
$resp->type = 'twitter:user';
$resp->color = '#1da1f2';
$resp->twitter_user_name = strval($matches[1] ?? '');
$curl = curl_init("https://api.twitter.com/2/users/by?usernames={$resp->twitter_user_name}&user.fields=description,entities,id,name,profile_image_url,protected,url,username,verified");
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . TWITTER_API_TOKEN,
'Accept: application/json',
],
]);
$twitUserResp = curl_exec($curl);
curl_close($curl);
$resp->twitter_user_info = json_decode($twitUserResp);
if(isset($resp->twitter_user_info->data[0]->name))
$resp->title = $resp->twitter_user_info->data[0]->name;
if(isset($resp->twitter_user_info->data[0]->profile_image_url))
$resp->image = $resp->twitter_user_info->data[0]->profile_image_url;
if(isset($resp->twitter_user_info->data[0]->description))
$resp->description = $resp->twitter_user_info->data[0]->description;
$resp->site_name = 'Twitter';
break;
}
break;
case 'youtu.be': case 'www.youtu.be': // www. doesn't work for this, but may as well cover it
$youtubeVideoId = substr($urlPath, 1);
case 'youtube.com': case 'www.youtube.com':
case 'youtube-nocookie.com': case 'www.youtube-nocookie.com':
parse_str($parsedUrl->getQuery(), $queryString);
if(!isset($youtubeVideoId) && $urlPath === '/watch')
$youtubeVideoId = $queryString['v'] ?? null;
if(!empty($youtubeVideoId)) {
$resp->type = 'youtube:video';
$resp->color = '#f00';
$resp->youtube_video_id = $youtubeVideoId;
if(isset($queryString['t']))
$resp->youtube_start_time = $queryString['t'];
if(isset($queryString['list']))
$resp->youtube_playlist = $queryString['list'];
if(isset($queryString['index']))
$resp->youtube_playlist_index = $queryString['index'];
$curl = curl_init("https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&id={$resp->youtube_video_id}&key=" . GOOGLE_API_KEY);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
],
]);
$youtubeResp = curl_exec($curl);
curl_close($curl);
$resp->youtube_video_info = json_decode($youtubeResp);
if(isset($resp->youtube_video_info->items[0]->snippet->title))
$resp->title = $resp->youtube_video_info->items[0]->snippet->title;
if(isset($resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url))
$resp->image = $resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url;
if(isset($resp->youtube_video_info->items[0]->snippet->description))
$resp->description = $resp->youtube_video_info->items[0]->snippet->description;
$resp->site_name = 'YouTube';
}
break;
}
} else {
http_response_code(404);
$resp->error = 'metadata:scheme';
}
if((empty($resp->type) || isset($continueRaw)) && in_array($parsedUrl->getScheme(), ['http', 'https'])) {
$curl = curl_init((string)$parsedUrl);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => true,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_PATH_AS_IS => true,
CURLOPT_NOBODY => true,
CURLOPT_HEADER => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_DEFAULT_PROTOCOL => 'https',
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible) Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml',
],
]);
$headers = curl_exec($curl);
if($headers === false) {
$resp->error = 'metadata:timeout';
$resp->errorMessage = curl_error($curl);
} else {
$headersRaw = explode("\r\n", trim($headers));
$statusCode = 200;
$headers = [];
foreach($headersRaw as $header) {
if(empty($header))
continue;
if(strpos($header, ':') === false) {
$headParts = explode(' ', $header);
if(isset($headParts[1]) && is_numeric($headParts[1]))
$statusCode = (int)$headParts[1];
$headers = [];
continue;
}
$headerParts = explode(':', $header, 2);
$headerParts[0] = mb_strtolower($headerParts[0]);
if(isset($headers[$headerParts[0]]))
$headers[$headerParts[0]] .= ', ' . trim($headerParts[1] ?? '');
else
$headers[$headerParts[0]] = trim($headerParts[1] ?? '');
}
try {
$contentType = new MediaType($headers['content-type'] ?? '');
} catch(InvalidArgumentException $ex) {
$contentType = new MediaType('application/octet-stream');
}
$resp->content_type = $contentType;
$isHTML = $contentType->match('text/html');
$isXHTML = $contentType->match('application/xhtml+xml');
if($isHTML || $isXHTML) {
curl_setopt_array($curl, [
CURLOPT_NOBODY => false,
CURLOPT_HEADER => false,
]);
$body = curl_exec($curl);
curl_close($curl);
$document = new DOMDocument;
if($isXHTML) {
$document->loadXML($body);
} else {
@$document->loadHTML('<?xml encoding="' . $contentType->getCharset() . '">' . $body);
foreach($document->childNodes as $child)
if($child->nodeType === XML_PI_NODE) {
$document->removeChild($child);
break;
}
$document->encoding = $contentType->getCharset();
}
$charSet = $document->encoding;
$resp->type = 'website';
$resp->title = '';
$isMetaTitle = false;
$titleTag = $document->getElementsByTagName('title');
foreach($titleTag as $tag) {
$resp->title = trim(mb_convert_encoding($tag->textContent, 'utf-8', $charSet));
break;
}
$metaTags = $document->getElementsByTagName('meta');
foreach($metaTags as $tag) {
$nameAttr = $tag->hasAttribute('name') ? $tag->getAttribute('name') : (
$tag->hasAttribute('property') ? $tag->getAttribute('property') : ''
);
$valueAttr = $tag->hasAttribute('value') ? $tag->getAttribute('value') : (
$tag->hasAttribute('content') ? $tag->getAttribute('content') : ''
);
$nameAttr = trim(mb_convert_encoding($nameAttr, 'utf-8', $charSet));
$valueAttr = trim(mb_convert_encoding($valueAttr, 'utf-8', $charSet));
if(empty($nameAttr) || empty($valueAttr))
continue;
switch($nameAttr) {
case 'og:title':
case 'twitter:title':
if(!$isMetaTitle) {
$isMetaTitle = true;
$resp->title = $valueAttr;
}
break;
case 'description':
case 'og:description':
case 'twitter:description':
if(!isset($resp->description))
$resp->description = $valueAttr;
break;
case 'og:site_name':
$resp->site_name = $valueAttr;
break;
case 'og:image':
case 'twitter:image':
$resp->image = $valueAttr;
break;
case 'theme-color':
$resp->color = $valueAttr;
break;
case 'og:type':
$resp->type = $valueAttr;
break;
}
}
} else {
$resp->is_image = $isImage = $contentType->match('image/*');
$resp->is_audio = $isAudio = $contentType->match('audio/*');
$resp->is_video = $isVideo = $contentType->match('video/*');
if($isImage || $isAudio || $isVideo) {
curl_close($curl);
$resp->media = new stdClass;
$ffmpeg = json_decode(shell_exec(sprintf('ffprobe -show_streams -show_format -print_format json -v quiet -i %s', escapeshellarg((string)$parsedUrl))));
if(!empty($ffmpeg)) {
if(!empty($ffmpeg->format)) {
$resp->media->confidence = empty($ffmpeg->format->probe_score) ? 0 : (intval($ffmpeg->format->probe_score) / 100);
if(!empty($ffmpeg->format->duration))
$resp->media->duration = floatval($ffmpeg->format->duration);
if(!empty($ffmpeg->format->size))
$resp->media->size = intval($ffmpeg->format->size);
if(!empty($ffmpeg->format->bit_rate))
$resp->media->bitrate = intval($ffmpeg->format->bit_rate);
if($isVideo || $isImage) {
if(!empty($ffmpeg->streams)) {
foreach($ffmpeg->streams as $stream) {
if(($stream->codec_type ?? null) !== 'video')
continue;
$resp->width = intval($stream->coded_width ?? $stream->width ?? -1);
$resp->height = intval($stream->coded_height ?? $stream->height ?? -1);
if(!empty($stream->display_aspect_ratio))
$resp->media->aspect_ratio = $stream->display_aspect_ratio;
if($isImage)
break;
}
}
}
if($isAudio) {
function eat_tags(stdClass $dest, stdClass $source): void {
if(!empty($source->title) || !empty($source->TITLE))
$dest->title = $source->title ?? $source->TITLE;
if(!empty($source->artist) || !empty($source->ARTIST))
$dest->artist = $source->artist ?? $source->ARTIST;
if(!empty($source->album) || !empty($source->ALBUM))
$dest->album = $source->album ?? $source->ALBUM;
if(!empty($source->date) || !empty($source->DATE))
$dest->date = $source->date ?? $source->DATE;
if(!empty($source->comment) || !empty($source->COMMENT))
$dest->comment = $source->comment ?? $source->COMMENT;
if(!empty($source->genre) || !empty($source->GENRE))
$dest->genre = $source->genre ?? $source->GENRE;
}
if(!empty($ffmpeg->format->tags)) {
$resp->media->tags = new stdClass;
eat_tags($resp->media->tags, $ffmpeg->format->tags);
} elseif(!empty($ffmpeg->streams)) {
// iterate over streams, fuck ogg
$resp->media->tags = new stdClass;
foreach($ffmpeg->streams as $stream) {
if(($stream->codec_type ?? null) === 'audio' && !empty($stream->tags)) {
eat_tags($resp->media->tags, $stream->tags);
if(!empty($resp->media->tags))
break;
}
}
}
if(empty($resp->title)) {
$audioTitle = '';
if(!empty($resp->media->tags->artist))
$audioTitle .= $resp->media->tags->artist . ' - ';
if(!empty($resp->media->tags->title))
$audioTitle .= $resp->media->tags->title;
if(!empty($resp->media->tags->date))
$audioTitle .= ' (' . $resp->media->tags->date . ')';
if(!empty($audioTitle))
$resp->title = $audioTitle;
}
if(empty($resp->description) && !empty($resp->media->tags->comment))
$resp->description = $resp->media->tags->comment;
}
}
}
if(UIH_INCLUDE_RAW)
$resp->ffmpeg = $ffmpeg;
} else curl_close($curl);
}
}
}
Stopwatch::stop();
$resp->took = Stopwatch::elapsed();
$respJson = json_encode($resp);
DB::prepare('REPLACE INTO `uih_metadata_cache` (`metadata_url`, `metadata_resp`) VALUES (UNHEX(:hash), :resp)')
->bind('hash', $urlHash)
->bind('resp', $respJson)
->execute();
}
} finally {
if(!empty($semaphore))
sem_release($semaphore);
if(is_file($semPath))
unlink($semPath);
}
echo $respJson ?? json_encode($resp);
return;
}
if($reqPath === '/') {
if($reqMethod !== 'GET') {
http_response_code(405);
return;
}
header('Content-Type: text/plain');
if($reqHead)
return;
echo 'Metadata lookup service - OK';
return;
}
http_response_code(404);