Merge pull request #66 from shikorism/develop

Release 20190123.0030
This commit is contained in:
shibafu 2019-01-23 00:31:08 +09:00 committed by GitHub
commit 8c73bda2ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 379 additions and 42 deletions

View File

@ -13,7 +13,8 @@ return \PhpCsFixer\Config::create()
'return_type_declaration' => true,
'new_with_braces' => true,
'no_empty_statement' => true,
'standardize_not_equals' => true
'standardize_not_equals' => true,
'single_quote' => true
])
->setFinder(
\PhpCsFixer\Finder::create()

View File

@ -8,7 +8,7 @@ a.k.a. shikorism.net
## 構成
- Laravel 5.5
- Bootstrap 4.0
- Bootstrap 4.2.1
## 実行環境
@ -46,8 +46,28 @@ docker-compose exec web php artisan key:generate
docker-compose exec web php artisan migrate
```
6. ファイルに書き込めるように権限を設定します。
```
docker-compose exec web chown -R www-data /var/www/html
```
7. 最後に `.env` を読み込み直すために起動し直します。
```
docker-compose up -d
```
これで準備は完了です。Tissue が動いていれば `http://localhost:4545/` でアクセスができます。
## デバッグ実行
```
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
```
で起動することにより、DB のポート`5432`を開放してホストマシンから接続できるようになります。
## 環境構築上の諸注意
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class SettingController extends Controller
{
public function profile()
{
return view('setting.profile');
}
public function updateProfile(Request $request)
{
$inputs = $request->all();
$validator = Validator::make($inputs, [
'display_name' => 'required|string|max:20'
], [], [
'display_name' => '名前'
]);
if ($validator->fails()) {
return redirect()->route('setting')->withErrors($validator)->withInput();
}
$user = Auth::user();
$user->display_name = $inputs['display_name'];
$user->save();
return redirect()->route('setting')->with('status', 'プロフィールを更新しました。');
}
public function privacy()
{
return view('setting.privacy');
}
public function updatePrivacy(Request $request)
{
$inputs = $request->all(['is_protected', 'accept_analytics']);
$user = Auth::user();
$user->is_protected = $inputs['is_protected'] ?? false;
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
$user->save();
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
}
// ( ◠‿◠ )☛ここに気づいたか・・・消えてもらう ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ
// public function password()
// {
// abort(501);
// }
}

View File

@ -83,7 +83,7 @@ SQL
))
->where('user_id', $user->id)
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
->orderBy(DB::raw("1"))
->orderBy(DB::raw('1'))
->get();
$dailySum = [];

View File

@ -11,7 +11,7 @@ class DLsiteResolver implements Resolver
if ($res->getStatusCode() === 200) {
$ogpResolver = new OGPResolver();
$metadata = $ogpResolver->parse($res->getBody());
$metadata->image = str_replace("img_sam.jpg", "img_main.jpg", $metadata->image);
$metadata->image = str_replace('img_sam.jpg', 'img_main.jpg', $metadata->image);
return $metadata;
} else {

View File

@ -0,0 +1,42 @@
<?php
namespace App\MetadataResolver;
class DeviantArtResolver 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);
$node = $xpath->query('//*[@id="pimp-preload"]/following-sibling::div//img')->item(0);
$srcset = $node->getAttribute('srcset');
$srcset_array = explode('w,', $srcset);
$src = end($srcset_array);
$src = preg_replace('~ \d+w$~', '', $src);
if (preg_match('~\.wixmp\.com$~', parse_url($src)['host'])) {
// アスペクト比を保ったまま、縦か横が最大700pxになるように変換する。
// Ref: https://support.wixmp.com/en/article/image-service-3835799
if (strpos($src, '/v1/fill/')) {
$src = preg_replace('~/v1/fill/w_\d+,h_\d+,q_\d+,strp~', '/v1/fit/w_700,h_700,q_70,strp', $src);
} else {
$src = $src . '/v1/fit/w_700,h_700,q_70,strp/image.jpg';
}
}
$metadata->image = $src;
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@ -25,7 +25,7 @@ class FantiaResolver implements Resolver
$ogpUrl = $node->getAttribute('content');
// 投稿に画像がない場合ogp.jpgでない場合のみ大きい画像に変換する
if ($ogpUrl != "http://fantia.jp/images/ogp.jpg") {
if ($ogpUrl != 'http://fantia.jp/images/ogp.jpg') {
preg_match("~https://fantia\.s3\.amazonaws\.com/uploads/post/file/{$postId}/ogp_(.*?)\.(jpg|png)~", $ogpUrl, $match);
$uuid = $match[1];
$extension = $match[2];

View File

@ -0,0 +1,21 @@
<?php
namespace App\MetadataResolver;
class FanzaResolver 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());
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $metadata->image);
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@ -2,6 +2,8 @@
namespace App\MetadataResolver;
use Carbon\Carbon;
class KomifloResolver implements Resolver
{
public function resolve(string $url): Metadata
@ -21,6 +23,8 @@ class KomifloResolver implements Resolver
$metadata->description = ($json['content']['attributes']['artists']['children'][0]['data']['name'] ?? '?') .
' - ' .
($json['content']['parents'][0]['data']['title'] ?? '?');
$metadata->image = $json['content']['cdn_public'] . '/564_mobile_large_3x/' . $json['content']['named_imgs']['cover']['filename'] . $json['content']['signature'];
$metadata->expires_at = Carbon::parse($json['content']['signature_expires'])->setTimezone(config('app.timezone'));
return $metadata;
} else {

View File

@ -14,6 +14,8 @@ class MetadataResolver implements Resolver
'~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class,
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
'~dmm\.co\.jp/~' => FanzaResolver::class,
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
'/.*/' => OGPResolver::class
];

View File

@ -18,12 +18,18 @@ class OGPResolver implements Resolver
public function parse(string $html): Metadata
{
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
$xpath = new \DOMXPath($dom);
$metadata = new Metadata();
$metadata->title = $this->findContent($xpath, '//meta[@*="og:title"]', '//meta[@*="twitter:title"]');
if (empty($metadata->title)) {
$nodes = $xpath->query('//title');
if ($nodes->length !== 0) {
$metadata->title = $nodes->item(0)->textContent;
}
}
$metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]');
$metadata->image = $this->findContent($xpath, '//meta[@*="og:image"]', '//meta[@*="twitter:image"]');

View File

@ -12,8 +12,8 @@ class PixivResolver implements Resolver
*/
public function thumbnailToMasterUrl(string $thumbnailUrl): string
{
$temp = str_replace("/c/128x128", "", $thumbnailUrl);
$largeUrl = str_replace("square1200.jpg", "master1200.jpg", $temp);
$temp = str_replace('/c/128x128', '', $thumbnailUrl);
$largeUrl = str_replace('square1200.jpg', 'master1200.jpg', $temp);
return $largeUrl;
}
@ -27,21 +27,21 @@ class PixivResolver implements Resolver
*/
public function proxize(string $pixivUrl): string
{
return str_replace("i.pximg.net", "i.pixiv.cat", $pixivUrl);
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
}
public function resolve(string $url): Metadata
{
preg_match("~illust_id=(\d+)~", parse_url($url)["query"], $match);
preg_match("~illust_id=(\d+)~", parse_url($url)['query'], $match);
$illustId = $match[1];
// 漫画ページかつページ数あり
if (strpos(parse_url($url)["query"], "mode=manga_big") && strpos(parse_url($url)["query"], "page=")) {
preg_match("~page=(\d+)~", parse_url($url)["query"], $match);
if (strpos(parse_url($url)['query'], 'mode=manga_big') && strpos(parse_url($url)['query'], 'page=')) {
preg_match("~page=(\d+)~", parse_url($url)['query'], $match);
$page = $match[1];
// 未ログインでは漫画ページを開けないため、URL を作品ページに変換する
$url = str_replace("mode=manga_big", "mode=medium", $url);
$url = str_replace('mode=manga_big', 'mode=medium', $url);
$client = new \GuzzleHttp\Client();
$res = $client->get($url);
@ -55,7 +55,7 @@ class PixivResolver implements Resolver
$illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl);
// 指定ページに変換
$illustUrl = str_replace("p0_master", "p{$page}_master", $illustUrl);
$illustUrl = str_replace('p0_master', "p{$page}_master", $illustUrl);
$metadata->image = $this->proxize($illustUrl);
@ -71,10 +71,10 @@ class PixivResolver implements Resolver
$metadata = $ogpResolver->parse($res->getBody());
// OGP がデフォルト画像であるようならなんとかして画像を取得する
if (strpos($metadata->image, "pixiv_logo.gif") || strpos($metadata->image, "pictures.jpg")) {
if (strpos($metadata->image, 'pixiv_logo.gif') || strpos($metadata->image, 'pictures.jpg')) {
// 作品ページの場合のみ対応
if (strpos(parse_url($url)["query"], "mode=medium")) {
if (strpos(parse_url($url)['query'], 'mode=medium')) {
preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}(_p0)?_square1200\.jpg~", $res->getBody(), $match);
$illustThumbnailUrl = $match[0];

View File

@ -52,12 +52,25 @@ class Formatter
$url = preg_replace('~/#!/~u', '/', $url);
// Sort query parameters
$query = parse_url($url, PHP_URL_QUERY);
if (!empty($query)) {
$url = str_replace_last('?' . $query, '', $url);
parse_str($query, $params);
$parts = parse_url($url);
if (!empty($parts['query'])) {
// Remove query parameters
$url = str_replace_last('?' . $parts['query'], '', $url);
if (!empty($parts['fragment'])) {
// Remove fragment identifier
$url = str_replace_last('#' . $parts['fragment'], '', $url);
} else {
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
$url = preg_replace('/#\z/u', '', $url);
}
parse_str($parts['query'], $params);
ksort($params);
$url = $url . '?' . http_build_query($params);
if (!empty($parts['fragment'])) {
$url .= '#' . $parts['fragment'];
}
}
return $url;

View File

@ -46,6 +46,9 @@
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"fix": [
"php-cs-fixer fix"
]
},
"config": {

View File

@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddExpiresOnMetadata extends Migration
{

6
docker-compose.debug.yml Normal file
View File

@ -0,0 +1,6 @@
version: "3"
services:
db:
ports:
- 5432:5432

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
@endif
</h6>
@if (!$user->is_protected)
@if (!$user->is_protected || $user->isMe())
<h6 class="font-weight-bold mt-4"><span class="oi oi-timer"></span> 現在のセッション</h6>
@if (isset($currentSession))
<p class="card-text mb-0">{{ $currentSession }}経過</p>

View File

@ -63,8 +63,8 @@
</div>
</a>
</div>
<p class="mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif

View File

@ -61,8 +61,8 @@
</div>
</a>
</div>
<p class="mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif

View File

@ -72,7 +72,7 @@
</p>
</a>
<div class="dropdown-divider"></div>
{{--<a href="#" class="dropdown-item">設定</a>--}}
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
</div>
</li>

View File

@ -6,7 +6,7 @@
@else
<ul class="list-group">
@foreach($results as $ejaculation)
<li class="list-group-item border-bottom-only pt-3 pb-3">
<li class="list-group-item border-bottom-only pt-3 pb-3 tis-word-wrap">
<!-- span -->
<div class="d-flex justify-content-between">
<h5>
@ -44,8 +44,8 @@
</div>
</a>
</div>
<p class="mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif

View File

@ -0,0 +1,22 @@
@extends('layouts.base')
@section('content')
<div class="container">
<div class="row">
<div class="col-lg-4">
<div class="list-group">
<div class="list-group-item disabled font-weight-bold">設定</div>
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting' ? 'active' : '' }}"
href="{{ route('setting') }}"><span class="oi oi-person mr-1"></span> プロフィール</a>
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.privacy' ? 'active' : '' }}"
href="{{ route('setting.privacy') }}"><span class="oi oi-shield mr-1"></span> プライバシー</a>
{{--<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.password' ? 'active' : '' }}"
href="{{ route('setting.password') }}"><span class="oi oi-key mr-1"></span> パスワード</a>--}}
</div>
</div>
<div class="tab-content col-lg-8">
@yield('tab-content')
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,33 @@
@extends('setting.base')
@section('title', 'プライバシー設定')
@section('tab-content')
<h3>プライバシー</h3>
<hr>
<form action="{{ route('setting.privacy.update') }}" method="post">
{{ csrf_field() }}
<div class="form-group">
<div class="custom-control custom-checkbox mb-2">
<input id="protected" name="is_protected" class="custom-control-input" type="checkbox" {{ (old('is_protected') ?? Auth::user()->is_protected ) ? 'checked' : '' }}>
<label class="custom-control-label" for="protected">全てのチェックイン履歴を非公開にする</label>
</div>
<div class="custom-control custom-checkbox">
<input id="accept-analytics" name="accept_analytics" class="custom-control-input" type="checkbox" {{ (old('accept_analytics') ?? Auth::user()->accept_analytics ) ? 'checked' : '' }}>
<label class="custom-control-label" for="accept-analytics">匿名での統計にチェックインデータを利用することに同意します</label>
</div>
</div>
<button type="submit" class="btn btn-primary mt-2">更新</button>
</form>
@endsection
@push('script')
<script>
$('#protected').on('change', function () {
if (!$(this).prop('checked')) {
alert('チェックイン履歴を公開に切り替えると、個別に非公開設定されているものを除いた全てのチェックインが誰でも閲覧できるようになります。\nご注意ください。');
}
});
</script>
@endpush

View File

@ -0,0 +1,32 @@
@extends('setting.base')
@section('title', 'プロフィール設定')
@section('tab-content')
<h3>プロフィール</h3>
<hr>
<form action="{{ route('setting.profile.update') }}" method="post">
{{ csrf_field() }}
<div class="from-group">
<label for="display_name">名前</label>
<input id="display_name" name="display_name" type="text" class="form-control {{ $errors->has('display_name') ? ' is-invalid' : '' }}"
value="{{ old('display_name') ?? Auth::user()->display_name }}" maxlength="20" autocomplete="off">
@if ($errors->has('display_name'))
<div class="invalid-feedback">{{ $errors->first('display_name') }}</div>
@endif
</div>
<div class="from-group mt-2">
<label for="name">ユーザー名</label>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">@</div>
</div>
<input id="name" name="name" type="text" class="form-control" value="{{ Auth::user()->name }}" disabled>
</div>
<small class="form-text text-muted">現在は変更できません。</small>
</div>
<button type="submit" class="btn btn-primary mt-4">更新</button>
</form>
@endsection

View File

@ -34,7 +34,7 @@
@else
<ul class="list-group">
@forelse ($ejaculations as $ejaculation)
<li class="list-group-item border-bottom-only pt-3 pb-3">
<li class="list-group-item border-bottom-only pt-3 pb-3 tis-word-wrap">
<!-- span -->
<div class="d-flex justify-content-between">
<h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5>
@ -69,8 +69,8 @@
</div>
</a>
</div>
<p class="mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
<p class="d-flex align-items-baseline mb-2 col-12 px-0">
<span class="oi oi-link-intact mr-1"></span><a class="overflow-hidden" href="{{ $ejaculation->link }}" target="_blank" rel="noopener">{{ $ejaculation->link }}</a>
</p>
</div>
@endif

View File

@ -29,6 +29,13 @@ Route::middleware('auth')->group(function () {
Route::get('/checkin/{id}/edit', 'EjaculationController@edit')->name('checkin.edit');
Route::put('/checkin/{id}', 'EjaculationController@update')->name('checkin.update');
Route::delete('/checkin/{id}', 'EjaculationController@destroy')->name('checkin.destroy');
Route::redirect('/setting', '/setting/profile', 301);
Route::get('/setting/profile', 'SettingController@profile')->name('setting');
Route::post('/setting/profile', 'SettingController@updateProfile')->name('setting.profile.update');
Route::get('/setting/privacy', 'SettingController@privacy')->name('setting.privacy');
Route::post('/setting/privacy', 'SettingController@updatePrivacy')->name('setting.privacy.update');
// Route::get('/setting/password', 'SettingController@password')->name('setting.password');
});
Route::get('/info', 'InfoController@index')->name('info');

View File

@ -36,9 +36,9 @@ class NijieResolverTest extends TestCase
sleep(1);
$resolver = new NijieResolver();
$metadata = $resolver->resolve('https://nijie.info/view.php?id=258078');
$this->assertEquals('騎乗位ルーミア | しょったれ', $metadata->title);
$this->assertEquals("最初は顔をZUN絵で描こうとか思っていたのだが、難しかったのでやめた", $metadata->description);
$metadata = $resolver->resolve('https://nijie.info/view.php?id=9537');
$this->assertEquals('ニジエがgifに対応したんだってね 奥さん | 黒末アプコ', $metadata->title);
$this->assertEquals('アニメgifとか専門外なのでよくわかりませんでした', $metadata->description);
$this->assertRegExp('~/nijie\.info/pic/logo~', $metadata->image);
}
@ -82,9 +82,9 @@ class NijieResolverTest extends TestCase
sleep(1);
$resolver = new NijieResolver();
$metadata = $resolver->resolve('https://sp.nijie.info/view.php?id=258078');
$this->assertEquals('騎乗位ルーミア | しょったれ', $metadata->title);
$this->assertEquals("最初は顔をZUN絵で描こうとか思っていたのだが、難しかったのでやめた", $metadata->description);
$metadata = $resolver->resolve('https://nijie.info/view.php?id=9537');
$this->assertEquals('ニジエがgifに対応したんだってね 奥さん | 黒末アプコ', $metadata->title);
$this->assertEquals('アニメgifとか専門外なのでよくわかりませんでした', $metadata->description);
$this->assertRegExp('~/nijie\.info/pic/logo~', $metadata->image);
}

View File

@ -25,4 +25,14 @@ class OGPResolverTest extends TestCase
$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);
}
public function testResolveTitleOnly()
{
$resolver = new OGPResolver();
$metadata = $resolver->resolve('http://example.com');
$this->assertEquals('Example Domain', $metadata->title);
$this->assertEmpty($metadata->description);
$this->assertEmpty($metadata->image);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Tests\Unit\Utilities;
use App\Utilities\Formatter;
use Tests\TestCase;
class FormatterTest extends TestCase
{
public function testNormalizeUrlWithoutQuery()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to';
$this->assertEquals($url, $formatter->normalizeUrl($url));
}
public function testNormalizeUrlWithSortedQuery()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to?foo=bar&hoge=fuga';
$this->assertEquals($url, $formatter->normalizeUrl($url));
}
public function testNormalizeUrlWithUnsortedQuery()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to?hoge=fuga&foo=bar';
$this->assertEquals('http://example.com/path/to?foo=bar&hoge=fuga', $formatter->normalizeUrl($url));
}
public function testNormalizeUrlWithSortedQueryAndFragment()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to?foo=bar&hoge=fuga#fragment';
$this->assertEquals($url, $formatter->normalizeUrl($url));
}
public function testNormalizeUrlWithFragment()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to#fragment';
$this->assertEquals($url, $formatter->normalizeUrl($url));
}
public function testNormalizeUrlWithSortedQueryAndZeroLengthFragment()
{
$formatter = new Formatter();
$url = 'http://example.com/path/to?foo=bar&hoge=fuga#';
$this->assertEquals('http://example.com/path/to?foo=bar&hoge=fuga', $formatter->normalizeUrl($url));
}
}