コンテンツ情報取得の実装をapi.phpから剥がした
This commit is contained in:
		
							
								
								
									
										10
									
								
								app/MetadataResolver/Metadata.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/MetadataResolver/Metadata.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | class Metadata | ||||||
|  | { | ||||||
|  |     public $title = ''; | ||||||
|  |     public $description = ''; | ||||||
|  |     public $image = ''; | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								app/MetadataResolver/MetadataResolver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/MetadataResolver/MetadataResolver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | class MetadataResolver implements Resolver | ||||||
|  | { | ||||||
|  |     public $rules = [ | ||||||
|  |         '~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class, | ||||||
|  |         '~nijie\.info/view\.php~' => NijieResolver::class, | ||||||
|  |         '/.*/' => OGPResolver::class | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     public function resolve(string $url): Metadata | ||||||
|  |     { | ||||||
|  |         foreach ($this->rules as $pattern => $class) { | ||||||
|  |             if (preg_match($pattern, $url) === 1) { | ||||||
|  |                 $resolver = new $class; | ||||||
|  |                 return $resolver->resolve($url); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         throw new \UnexpectedValueException('URL not matched.'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								app/MetadataResolver/NicoSeigaResolver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/MetadataResolver/NicoSeigaResolver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | class NicoSeigaResolver implements Resolver | ||||||
|  | { | ||||||
|  |     public function resolve(string $url): Metadata | ||||||
|  |     { | ||||||
|  |         $client = new \GuzzleHttp\Client(); | ||||||
|  |         $res = $client->get($url); | ||||||
|  |         if ($res->getStatusCode() === 200) { | ||||||
|  |             $ogpResolver = new OGPResolver(); | ||||||
|  |             $metadata = $ogpResolver->parse($res->getBody()); | ||||||
|  |  | ||||||
|  |             // ページURLからサムネイルURLに変換 | ||||||
|  |             preg_match('~http://(?:(?:sp\\.)?seiga\\.nicovideo\\.jp/seiga(?:/#!)?|nico\\.ms)/im(\\d+)~', $url, $matches); | ||||||
|  |             $metadata->image = "http://lohas.nicoseiga.jp/thumb/${matches[1]}l?"; | ||||||
|  |  | ||||||
|  |             return $metadata; | ||||||
|  |         } else { | ||||||
|  |             throw new \RuntimeException("{$res->getStatusCode()}: $url"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								app/MetadataResolver/NijieResolver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/MetadataResolver/NijieResolver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | class NijieResolver implements Resolver | ||||||
|  | { | ||||||
|  |     public function resolve(string $url): Metadata | ||||||
|  |     { | ||||||
|  |         $client = new \GuzzleHttp\Client(); | ||||||
|  |         $res = $client->get($url); | ||||||
|  |         if ($res->getStatusCode() === 200) { | ||||||
|  |             $ogpResolver = new OGPResolver(); | ||||||
|  |             $metadata = $ogpResolver->parse($res->getBody()); | ||||||
|  |  | ||||||
|  |             $dom = new \DOMDocument(); | ||||||
|  |             @$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8')); | ||||||
|  |             $xpath = new \DOMXPath($dom); | ||||||
|  |             $dataNode = $xpath->query('//script[substring(@type, string-length(@type) - 3, 4) = "json"]'); | ||||||
|  |             foreach ($dataNode as $node) { | ||||||
|  |                 // 改行がそのまま入っていることがあるのでデコード前にエスケープが必要 | ||||||
|  |                 $imageData = json_decode(preg_replace('/\r?\n/', '\n', $node->nodeValue), true); | ||||||
|  |                 if (isset($imageData['thumbnailUrl'])) { | ||||||
|  |                     $metadata->image = preg_replace('~nijie\\.info/.*/nijie_picture/~', 'nijie.info/nijie_picture/', $imageData['thumbnailUrl']); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return $metadata; | ||||||
|  |         } else { | ||||||
|  |             throw new \RuntimeException("{$res->getStatusCode()}: $url"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								app/MetadataResolver/OGPResolver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								app/MetadataResolver/OGPResolver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | class OGPResolver implements Resolver | ||||||
|  | { | ||||||
|  |     public function resolve(string $url): Metadata | ||||||
|  |     { | ||||||
|  |         $client = new \GuzzleHttp\Client(); | ||||||
|  |         $res = $client->get($url); | ||||||
|  |         if ($res->getStatusCode() === 200) { | ||||||
|  |             return $this->parse($res->getBody()); | ||||||
|  |         } else { | ||||||
|  |             throw new \RuntimeException("{$res->getStatusCode()}: $url"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function parse(string $html): Metadata | ||||||
|  |     { | ||||||
|  |         $dom = new \DOMDocument(); | ||||||
|  |         @$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); | ||||||
|  |         $xpath = new \DOMXPath($dom); | ||||||
|  |  | ||||||
|  |         $metadata = new Metadata(); | ||||||
|  |  | ||||||
|  |         $titleNode = $xpath->query('//meta[@*="og:title"]'); | ||||||
|  |         foreach ($titleNode as $node) { | ||||||
|  |             if (!empty($node->getAttribute('content'))) { | ||||||
|  |                 $metadata->title = $node->getAttribute('content'); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $descriptionNode = $xpath->query('//meta[@*="og:description"]'); | ||||||
|  |         foreach ($descriptionNode as $node) { | ||||||
|  |             if (!empty($node->getAttribute('content'))) { | ||||||
|  |                 $metadata->description = $node->getAttribute('content'); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $imageNode = $xpath->query('//meta[@*="og:image"]'); | ||||||
|  |         foreach ($imageNode as $node) { | ||||||
|  |             if (!empty($node->getAttribute('content'))) { | ||||||
|  |                 $metadata->image = $node->getAttribute('content'); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $metadata; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								app/MetadataResolver/Resolver.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/MetadataResolver/Resolver.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\MetadataResolver; | ||||||
|  |  | ||||||
|  | interface Resolver | ||||||
|  | { | ||||||
|  |     public function resolve(string $url): Metadata; | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| namespace App\Providers; | namespace App\Providers; | ||||||
|  |  | ||||||
|  | use App\MetadataResolver\MetadataResolver; | ||||||
| use Illuminate\Support\ServiceProvider; | use Illuminate\Support\ServiceProvider; | ||||||
|  |  | ||||||
| class AppServiceProvider extends ServiceProvider | class AppServiceProvider extends ServiceProvider | ||||||
| @@ -23,6 +24,8 @@ class AppServiceProvider extends ServiceProvider | |||||||
|      */ |      */ | ||||||
|     public function register() |     public function register() | ||||||
|     { |     { | ||||||
|         // |         $this->app->singleton(MetadataResolver::class, function ($app) { | ||||||
|  |             return new MetadataResolver(); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
|  | use App\MetadataResolver\MetadataResolver; | ||||||
| use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||||
|  |  | ||||||
| /* | /* | ||||||
| @@ -17,75 +18,16 @@ Route::middleware('auth:api')->get('/user', function (Request $request) { | |||||||
|     return $request->user(); |     return $request->user(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| Route::get('/checkin/card', function (Request $request) { | Route::get('/checkin/card', function (Request $request, MetadataResolver $resolver) { | ||||||
|     $request->validate([ |     $request->validate([ | ||||||
|         'url:required|url' |         'url:required|url' | ||||||
|     ]); |     ]); | ||||||
|     $url = $request->input('url'); |     $url = $request->input('url'); | ||||||
|  |  | ||||||
|     $client = new GuzzleHttp\Client(); |     $metadata = $resolver->resolve($url); | ||||||
|     $res = $client->get($url); |     $response = response()->json($metadata); | ||||||
|     if ($res->getStatusCode() === 200) { |  | ||||||
|         $dom = new DOMDocument(); |  | ||||||
|         @$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8')); |  | ||||||
|         $xpath = new DOMXPath($dom); |  | ||||||
|  |  | ||||||
|         $result = [ |  | ||||||
|             'title' => '', |  | ||||||
|             'description' => '', |  | ||||||
|             'image' => '' |  | ||||||
|         ]; |  | ||||||
|  |  | ||||||
|         $titleNode = $xpath->query('//meta[@*="og:title"]'); |  | ||||||
|         foreach ($titleNode as $node) { |  | ||||||
|             if (!empty($node->getAttribute('content'))) { |  | ||||||
|                 $result['title'] = $node->getAttribute('content'); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $descriptionNode = $xpath->query('//meta[@*="og:description"]'); |  | ||||||
|         foreach ($descriptionNode as $node) { |  | ||||||
|             if (!empty($node->getAttribute('content'))) { |  | ||||||
|                 $result['description'] = $node->getAttribute('content'); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $imageNode = $xpath->query('//meta[@*="og:image"]'); |  | ||||||
|         foreach ($imageNode as $node) { |  | ||||||
|             if (!empty($node->getAttribute('content'))) { |  | ||||||
|                 $result['image'] = $node->getAttribute('content'); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // 一部サイトについては別のサムネイルの取得を試みる |  | ||||||
|         if (mb_strpos($url, 'nico.ms/im') !== false || |  | ||||||
|             mb_strpos($url, 'seiga.nicovideo.jp/seiga/im') !== false || |  | ||||||
|             mb_strpos($url, 'sp.seiga.nicovideo.jp/seiga/#!/im') !== false) { |  | ||||||
|             // ニコニコ静画用の処理 |  | ||||||
|             preg_match('~http://(?:(?:sp\\.)?seiga\\.nicovideo\\.jp/seiga(?:/#!)?|nico\\.ms)/im(\\d+)~', $url, $matches); |  | ||||||
|             $result['image'] = "http://lohas.nicoseiga.jp/thumb/${matches[1]}l?"; |  | ||||||
|         } elseif (mb_strpos($url, 'nijie.info/view.php')) { |  | ||||||
|             // ニジエ用の処理 |  | ||||||
|             $dataNode = $xpath->query('//script[substring(@type, string-length(@type) - 3, 4) = "json"]'); |  | ||||||
|             foreach ($dataNode as $node) { |  | ||||||
|                 // 改行がそのまま入っていることがあるのでデコード前にエスケープが必要 |  | ||||||
|                 $imageData = json_decode(preg_replace('/\r?\n/', '\n', $node->nodeValue), true); |  | ||||||
|                 if (isset($imageData['thumbnailUrl'])) { |  | ||||||
|                     $result['image'] = preg_replace('~nijie\\.info/.*/nijie_picture/~', 'nijie.info/nijie_picture/', $imageData['thumbnailUrl']); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $response = response()->json($result); |  | ||||||
|     if (!config('app.debug')) { |     if (!config('app.debug')) { | ||||||
|         $response = $response->setCache(['public' => true, 'max_age' => 86400]); |         $response = $response->setCache(['public' => true, 'max_age' => 86400]); | ||||||
|     } |     } | ||||||
|     return $response; |     return $response; | ||||||
|     } else { |  | ||||||
|         abort($res->getStatusCode()); |  | ||||||
|     } |  | ||||||
| }); | }); | ||||||
							
								
								
									
										28
									
								
								tests/Unit/MetadataResolver/OGPResolverTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tests/Unit/MetadataResolver/OGPResolverTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Tests\Unit\MetadataResolver; | ||||||
|  |  | ||||||
|  | use App\MetadataResolver\OGPResolver; | ||||||
|  | use GuzzleHttp\Exception\ClientException; | ||||||
|  | use Tests\TestCase; | ||||||
|  |  | ||||||
|  | class OGPResolverTest extends TestCase | ||||||
|  | { | ||||||
|  |     public function testMissingUrl() | ||||||
|  |     { | ||||||
|  |         $resolver = new OGPResolver(); | ||||||
|  |  | ||||||
|  |         $this->expectException(ClientException::class); | ||||||
|  |         $resolver->resolve('http://example.com/404'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function testResolve() | ||||||
|  |     { | ||||||
|  |         $resolver = new OGPResolver(); | ||||||
|  |  | ||||||
|  |         $metadata = $resolver->resolve('http://ogp.me'); | ||||||
|  |         $this->assertEquals('Open Graph protocol', $metadata->title); | ||||||
|  |         $this->assertEquals('The Open Graph protocol enables any web page to become a rich object in a social graph.', $metadata->description); | ||||||
|  |         $this->assertEquals('http://ogp.me/logo.png', $metadata->image); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 shibafu
					shibafu