Skip to content

Commit

Permalink
[PixivBridge] Add cookie auth and options (RSS-Bridge#3653)
Browse files Browse the repository at this point in the history
* added cookie mgmt and support for issue
RSS-Bridge#2759

* added image proxy option

* + mature and ai options, + cookie doc

* mention doc

* check cookie is auth'd
  • Loading branch information
mruac authored Sep 6, 2023
1 parent dbe37cc commit b3a7842
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 19 deletions.
201 changes: 182 additions & 19 deletions bridges/PixivBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ class PixivBridge extends BridgeAbstract
const NAME = 'Pixiv Bridge';
const URI = 'https://www.pixiv.net/';
const DESCRIPTION = 'Returns the tag search from pixiv.net';
const MAINTAINER = 'mruac';
const CONFIGURATION = [
'cookie' => [
'required' => false,
'defaultValue' => null
],
'proxy_url' => [
'required' => false,
'defaultValue' => null
]
];


const PARAMETERS = [
Expand All @@ -23,11 +34,21 @@ class PixivBridge extends BridgeAbstract
'mode' => [
'name' => 'Post Type',
'type' => 'list',
'values' => ['All Works' => 'all',
'Illustrations' => 'illustrations/',
'Manga' => 'manga/',
'Novels' => 'novels/']
'values' => [
'All Works' => 'all',
'Illustrations' => 'illustrations/',
'Manga' => 'manga/',
'Novels' => 'novels/'
]
],
'mature' => [
'name' => 'Include R-18 works',
'type' => 'checkbox'
],
'ai' => [
'name' => 'Include AI-Generated works',
'type' => 'checkbox'
]
],
'Tag' => [
'tag' => [
Expand Down Expand Up @@ -76,7 +97,7 @@ public function getName()
default:
return parent::getName();
}
return 'Pixiv ' . $this->getKey('mode') . " from ${context} ${query}";
return 'Pixiv ' . $this->getKey('mode') . " from {$context} {$query}";
}

public function getURI()
Expand Down Expand Up @@ -106,7 +127,7 @@ private function getSearchURI($mode)
break;
case 'User':
$uri = static::URI . 'ajax/user/' . $this->getInput('userid')
. '/profile/top';
. '/profile/top';
break;
default:
returnClientError('Invalid Context');
Expand All @@ -116,18 +137,47 @@ private function getSearchURI($mode)

private function getDataFromJSON($json, $json_key)
{
$json = $json['body'][$json_key];
$key = $json_key;
if (
$this->queriedContext === 'Tag' &&
$this->getOption('cookie') !== null
) {
switch ($json_key) {
case 'illust':
case 'manga':
$key = 'illustManga';
break;
}
}
$json = $json['body'][$key];
// Tags context contains subkey
if ($this->queriedContext == 'Tag') {
if ($this->queriedContext === 'Tag') {
$json = $json['data'];
if ($this->getOption('cookie') !== null) {
switch ($json_key) {
case 'illust':
$json = array_reduce($json, function ($acc, $i) {
if ($i['illustType'] === 0) {
$acc[] = $i;
}return $acc;
}, []);
break;
case 'manga':
$json = array_reduce($json, function ($acc, $i) {
if ($i['illustType'] === 1) {
$acc[] = $i;
}return $acc;
}, []);
break;
}
}
}
return $json;
}

private function collectWorksArray()
{
$content = getContents($this->getSearchURI($this->getInput('mode')));
$content = json_decode($content, true);
$content = $this->getData($this->getSearchURI($this->getInput('mode')), true, true);
if ($this->getInput('mode') == 'all') {
$total = [];
foreach (self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) {
Expand All @@ -144,14 +194,36 @@ private function collectWorksArray()

public function collectData()
{
$this->checkOptions();
$proxy_url = $this->getOption('proxy_url');
$proxy_url = $proxy_url ? rtrim($proxy_url, '/') : null;

$content = $this->collectWorksArray();
$content = array_filter($content, function ($v, $k) {
return !array_key_exists('isAdContainer', $v);
}, ARRAY_FILTER_USE_BOTH);

// Sort by updateDate to get newest works
usort($content, function ($a, $b) {
return $b['updateDate'] <=> $a['updateDate'];
});

//exclude AI generated works if unchecked.
if ($this->getInput('ai') !== true) {
$content = array_filter($content, function ($v) {
$isAI = $v['aiType'] === 2;
return !$isAI;
});
}

//exclude R-18 works if unchecked.
if ($this->getInput('mature') !== true) {
$content = array_filter($content, function ($v) {
$isMature = $v['xRestrict'] > 0;
return !$isMature;
});
}

$content = array_slice($content, 0, $this->getInput('posts'));

foreach ($content as $result) {
Expand All @@ -168,12 +240,25 @@ public function collectData()
$item['author'] = $result['userName'];
$item['timestamp'] = $result['updateDate'];
$item['categories'] = $result['tags'];
$cached_image = $this->cacheImage(
$result['url'],
$result['id'],
array_key_exists('illustType', $result)
);
$item['content'] = "<img src='" . $cached_image . "' />";

if ($proxy_url) {
//use proxy image host if set.
if ($this->getInput('fullsize')) {
$ajax_uri = static::URI . 'ajax/illust/' . $result['id'];
$imagejson = $this->getData($ajax_uri, true, true);
$img_url = preg_replace('/https:\/\/i\.pximg\.net/', $proxy_url, $imagejson['body']['urls']['original']);
} else {
$img_url = preg_replace('/https:\/\/i\.pximg\.net/', $proxy_url, $result['url']);
}
} else {
//else cache and use image.
$img_url = $this->cacheImage(
$result['url'],
$result['id'],
array_key_exists('illustType', $result)
);
}
$item['content'] = "<img src='" . $img_url . "' />";

// Additional content items
if (array_key_exists('pageCount', $result)) {
Expand All @@ -188,6 +273,7 @@ public function collectData()

/**
* todo: remove manual file cache
* See bridge specific documentation for alternative option.
*/
private function cacheImage($url, $illustId, $isImage)
{
Expand All @@ -209,19 +295,96 @@ private function cacheImage($url, $illustId, $isImage)
// Get fullsize URL
if ($isImage && $this->getInput('fullsize')) {
$ajax_uri = static::URI . 'ajax/illust/' . $illustId;
$imagejson = json_decode(getContents($ajax_uri), true);
$imagejson = $this->getData($ajax_uri, true, true);
$url = $imagejson['body']['urls']['original'];
}

$headers = ['Referer: ' . static::URI];
try {
$illust = getContents($url, $headers);
$illust = $this->getData($url, true, false, $headers);
} catch (Exception $e) {
$illust = getContents($thumbnailurl, $headers); // Original thumbnail
$illust = $this->getData($thumbnailurl, true, false, $headers); // Original thumbnail
}
file_put_contents($path, $illust);
}

return get_home_page_url() . 'cache/pixiv_img/' . preg_replace('/.*\//', '', $path);
}

private function checkOptions()
{
$proxy = $this->getOption('proxy_url');
if ($proxy) {
if (
!(strlen($proxy) > 0 && preg_match('/https?:\/\/.*/', $proxy))
) {
return returnServerError('Invalid proxy_url value set. The proxy must include the HTTP/S at the beginning of the url.');
}
}

$cookie = $this->getCookie();
if ($cookie) {
$isAuth = $this->loadCacheValue('is_authenticated');
if (!$isAuth) {
$res = $this->getData('https://www.pixiv.net/ajax/webpush', true, true)
or returnServerError('Invalid PHPSESSID cookie provided. Please check the 🍪 and try again.');
if ($res['error'] === false) {
$this->saveCacheValue('is_authenticated', true);
}
}
}
}

private function checkCookie(array $headers)
{
if (array_key_exists('set-cookie', $headers)) {
foreach ($headers['set-cookie'] as $value) {
if (str_starts_with($value, 'PHPSESSID=')) {
parse_str(strtr($value, ['&' => '%26', '+' => '%2B', ';' => '&']), $cookie);
if ($cookie['PHPSESSID'] != $this->getCookie()) {
$this->saveCacheValue('cookie', $cookie['PHPSESSID']);
}
break;
}
}
}
}

private function getCookie()
{
// checks if cookie is set, if not initialise it with the cookie from the config
$value = $this->loadCacheValue('cookie', 2678400 /* 30 days + 1 day to let cookie chance to renew */);
if (!isset($value)) {
$value = $this->getOption('cookie');
$this->saveCacheValue('cookie', $this->getOption('cookie'));
}
return $value;
}

//Cache getContents by default
private function getData(string $url, bool $cache = true, bool $getJSON = false, array $httpHeaders = [], array $curlOptions = [])
{
$cookie_str = $this->getCookie();
if ($cookie_str) {
$curlOptions[CURLOPT_COOKIE] = 'PHPSESSID=' . $cookie_str;
}

if ($cache) {
$data = $this->loadCacheValue($url, 86400); // 24 hours
if (!$data) {
$data = getContents($url, $httpHeaders, $curlOptions, true) or returnServerError("Could not load $url");
$this->saveCacheValue($url, $data);
}
} else {
$data = getContents($url, $httpHeaders, $curlOptions, true) or returnServerError("Could not load $url");
}

$this->checkCookie($data['headers']);

if ($getJSON) {
return json_decode($data['content'], true);
} else {
return $data['content'];
}
}
}
23 changes: 23 additions & 0 deletions docs/10_Bridge_Specific/PixivBridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PixivBridge
===============

# Image proxy
As Pixiv requires images to be loaded with the `Referer "https://www.pixiv.net/"` header set, caching or image proxy is required to use this bridge.

To turn off image caching, set the `proxy_url` value in this bridge's configuration section of `config.ini.php` to the url of the proxy. The bridge will then use the proxy in this format (essentially replacing `https://i.pximg.net` with the proxy):

Before: `https://i.pximg.net/img-original/img/0000/00/00/00/00/00/12345678_p0.png`

After: `https://proxy.example.com/img-original/img/0000/00/00/00/00/00/12345678_p0.png`

```
proxy_url = "https://proxy.example.com"
```

# Authentication
Authentication is required to view and search R-18+ and non-public images. To enable this, set the following in this bridge's configuration in `config.ini.php`.

```
; from cookie "PHPSESSID". Recommend to get in incognito browser.
cookie = "00000000_hashedsessionidhere"
```

0 comments on commit b3a7842

Please sign in to comment.