그누보드 Hook 사용법

최근 그누보드 업데이트 스크립트를 만들면서, 코어 코드가 수정되었을때 자동으로 업데이트 해주는 기능 구현에 애를 먹고 있다.
하지만 그누보드 5.4부터 새로 추가된 hook을 사용하면 코어코드에 대한 수정을 최소한으로 하거나 혹은 코어 수정없이 진행가능하기에 업데이트가 용이하다.

Hook

Hook은 단어 그대로 갈고리처럼 코드 중간에 갈고리를 걸어, 코드를 실행시켜주는 기능을 한다.
혹은 이벤트-리스너와 같은 기능을 한다고 생각해도 좋다.

그누보드가 Hook을 어떻게 구현 했는지 궁금하면, https://github.com/Josantonius/PHP-Hook 의 코드를 가져다 썻다고 하니 보도록 하고 사용 방법만 간단히 익혀보자.

구현 내용 및 분석

간단히 1대1 문의가 왓을 때 slack이나 기타 메신저로 알려주는 부분을 구현해보자.

먼저 그누보드에선 1대1문의를 작성할 때 /bbs/qawrite.php에서 작성을 한 뒤 /bbs/qawrite_update.php로 데이터를 전송해 DB에 저장한다.

이때 /bbs/qawrite_update.php파일 321줄에

<?php
# ...
run_event('qawrite_update', $qa_id, $write, $w, $qaconfig);
# ...

run_event 함수에서 qawrite_update 라는 이벤트를 $qa_id, $write, $w, $qaconfig 변수 4개와 함께 실행하고(일으키고)있다.

하지만 현재는 qawrite_update라는 이벤트에 아무런 내용이 없으므로 아무런 기능을 하지 않는다.
이제 바깥(코어 파일이 아닌 곳)에서 qawrite_update 이벤트가 실행될 때 slack 메시지를 보내는 기능을 추가해보자.

구현

그누보드는 기본적으로 /extend 폴더에 있는 파일은 외부 플러그인 취급하며, 따로 include('common.php');해주지 않아도 자동으로 .php로 끝나는 파일은 common.php가 로딩된 후 실행시켜준다. (common.php 659~667번째줄)

/extend/slack.notice.php

<?php
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가

add_event('qawrite_update', 'slack_notice', G5_HOOK_DEFAULT_PRIORITY, 4);

function slack_notice($qa_id, $write, $w, $qaconfig)
{
    global $g5;

    if($w == '' || $w == 'r') {
        $text = "";
        $qa = sql_fetch("SELECT * FROM `{$g5['qa_content_table']}` WHERE `qa_id` = {$qa_id}");
        $text .= "[{$qa['qa_category']}] {$qa['qa_subject']}\n\t=> {$qa['qa_content']}";

        $slack = new Slack();
        $slack = $slack->sendMessage($text);
    }
}

위 코드를 간략히 분석해보면
add_event >
1. qawrite_update 이벤트가 실행될 때, ‘slack_notice‘ 라는 함수를 실행해라. (Variable function)
2. qawrite_update이벤트가 실행될 때 등록된 함수들 중에서 G5_HOOK_DEFAULT_PRIORITY(8) 번째로 실행 해라. (이를 이용하면 순서의 의존적인 여러 함수를 순차적으로 실행 할 수 있다.)
3. 전달되는 파라미터는 총4개 이다.

function slack_notice >
1. 인자로 $qa_id, $write, $w, $qaconfig를 전달받고,
2. 만약 새로운 등록($w == '')이거나, 재문의($w == 'r') 일경우
3. 문의 카테고리(qa_category), 제목(qa_subject), 내용(qa_content)을 담아 slack으로 전송한다.

정리

이 처럼 미리 run_event 가 들어 있는 곳에 내용을 보고 코어 파일에 수정없이 기능을 추가할 수 있어서, 이후 업데이트할 때 변경된 파일을 일일히 신경쓰지 않고 바로바로 엎을 수 있다.

또한, 원하는 위치에 내용이 없다고 하더라고 해당부분에 run_event('원하는_이벤트_이름', $arg1, $arg2) 이렇게 한 줄만 추가해 주고 extend 같은 외부 파일에서 add_event()함수를 통해 실행해주면 코어 코드에 대한 수정을 최소화 할 수 있다.

마지막으로 위에서본 add_event ( 이벤트 추가 )와 run_event ( 이벤트 실행 )말고도

add_replace ( 내용 변경 )
run_replace ( 내용 변경 적용 )
delete_event ( 이벤트 추가된 것을 취소함 )
delete_replace ( 내용 변경된 것을 취소함 )와 같은 함수들이 있으니 그누보드 메뉴얼에서 내용을 확인하고 다양하게 사용해볼 수 있다.

마무리

곧 훈련소 가는데(크흡..) 가기전에 블로그 업데이트 한번 해고 싶어서 최근 유용하게 쓰고있는 그누보드 hook에 대한 내용을 정리해 봤습니다.
기분이 싱숭생숭하다보니 내용이 중구난방일 것같은데 잘못된 것이 있다면 댓글로 남겨주시면 훈련소 다녀와서(ㅎ) 수정하겠습니다.

[패치됨] Laravel 7.x – XSS vulnerability

이 포스팅은 laravel 7.0 ~ 7.1.2 까지 버전에 존재하는 취약점 입니다.
사용중이신 버전이 포함되어있다면 laravel 7.1.3이상으로 업데이트 하시기 바랍니다.

Laravel news of XSS vector

laravel-news 에서 7.1.2 미만의 버전에서 XSS 공격 포인트가 있으니 업데이트하라는 소식이 들려와 어느부분이 취약한지 궁금해 해당 부분에 대한 코드를 찾아 봤다.

Github를 의 커밋 로그를 보면 attributesToString 메소드에 $escapeBound 옵션이 추가되었고, sanitizeComponentAttribute란 static 메소드가 추가되었다.

sanitizeComponentAttribute 메소드는 입력된 값이 문자열이거나, object 인경우 __toString 매직 메소드가 존재하면 e()(laravel의 html 이스케이프 함수)로 string을 소독(?) 하는 메소드이다.

근데 x-blade 문법에서 attributes->merge하는 메소드에서는 해당 코드가 적용되지 않아 tag 부분을 이스케이프 할 수 있어 XSS 공격이 가능해진다.

XSS code

<!-- resources/views/welcome.blade.php -->
<x-xss></x-xss>
<!-- resources/views/components/xss.blade.php -->
<div {{ $attributes->merge(['class' => request()->css]) }}>XSS able</div>

URI : http://127.0.0.1:8000/?css="><script>alert(1)</script>

<!-- Result -->
<div class="\"><script>alert(1)</script>">XSS able</div>
XSS in laravel 7.1.2

위와 같이 XSS가 실행되는 것을 볼 수 있다.

Patch

메일 보내고 1시간 만에 패치하고 릴리즈 되었다 ㄷㄷ;

7.1.3 버전에서 패치되었으며[commit] composer update를 하면 자동으로 패치되어진다.

나도 업데이트에서 언급 해줫으면 좋겟다. 나도 찾은건 찾은건데… ㅜ

[PHP] Namespace의 범위

TL;DR

PHP의 Namespace의 범위는 include, require의 상관없이 해당 파일에서만 영향을 끼친다.

Namespace

PHP 5.3부터 추가된 같은 이름의 함수, 클래스가 있을 때 네임스페이스로 각각 격리하여 사용할 수 있게 해주는 기능이다.

Simple example

spaceA.php

<?php

namespace A;

echo __FILE__ . ' > ' . __NAMESPACE__ . PHP_EOL;

function atest() { echo "spaceA atest function\n"; }

spaceB.php

<?php

echo __FILE__ . ' > ' . __NAMESPACE__ . PHP_EOL;

function btest() { echo "spaceB btest function\n"; }

index.php

<?php

namespace INDEX;

require('spaceA.php');
require('spaceB.php');

echo __FILE__ . ' > ' . __NAMESPACE__ . PHP_EOL;

btest();
atest();

php index.php로 php를 실행시키면 결과는 다음과 같다.

/home/silnex/test/spaceA.php > A
/home/silnex/test/spaceB.php > 
/home/silnex/test/index.php > INDEX
spaceB btest function
PHP Fatal error:  Uncaught Error: Call to undefined function INDEX\atest() in /home/silnex/test/index.php:11
Stack trace:
#0 {main}
  thrown in /home/silnex/test/index.php on line 11

require('spaceA.php') 의 결과가 출력된 1번째 줄을 보면
네임스페이스는 index.php에서 선언된 INDEX가 아닌 spaceA.php 파일에서 선언된 네임 스페이스다.

require('spaceB.php')의 결과가 출력된 2번째 줄을 보면
네임스페이스는 INDEX도 A도 아닌 아무 선언 안된 것을 볼 수 있다.
즉, 네임스페이스는 다른파일엔 영향을 끼치지 않는다는 걸 알 수 있다.

btest()의 실행 결과인 4번째 줄을 보면,
정상적으로 실행되고 있으나,
atest()의 경우 5번째줄 부터 표시된 에러가 나는것을 볼 수 있다.

이를 통해 함수가 받는 영향을 알 수 있는데,
기본적으로 Namespace가 없는 함수를 먼저 호출 하고, 만약 함수가 없다면,
현재 파일의 Namespace의 함수를 찾는 것을 확인 할 수 있다.

Laravel 6 to 7 Upgrade

3월 3일 laravel 7이 정식 릴리즈 되면서 기대하던 blade-x, stub, custom casts 등과 같은 기능들이 추가되었다.

지금 열심히 modern php 멤버 분들께서 도큐먼트를 번역중이시니 함께 번역을 하는것은 어떨가 싶다.

이번 글에선 Laravel 6에서 7으로 업데이트할 때 수정되야 하는 사항들을 간략히 정리해보겠다.

composer.json

  1. “php” 의 버전을  “^7.2.5”,
  2. “laravel/framework” 의 버전을 “^7.0″으로
  3. “nunomaduro/collision” 의 버전을 “^4.1″로
  4. “phpunit/phpunit” 의 버전을 “^8.5″로
  5. “facade/ignition” 의 버전을 “^2.0″으로

app/Exceptions/Handler.php

위에서 부터 순서대로
# 6.x
7.x

변경 사항이다.

<?php
# use Exception;
use Throwable;
// ...
# public function report(Exception $exception)
public function report(Throwable $exception)
// ...
# public function render($request, Exception $exception)
public function render($request, Throwable $exception)
// ...

위와같이 composer.json과 Handler.php 파일을 수정했다면
compsoer update를 통해 laravel 7으로 넘어가도록 하자!

[Laravel 7] Zttp

원문: laravel-news

Zttp is coming to Laravel 7

Adam Wathan가 만든 Zttp(Guzzle wapper) 패키지는 Laravel 7에서 새로운 Http 패키지로 선보여 집니다.

이 기능을 추가하기위한 pull request를 보면 Guzzle를 사용할 때보다 약 90%정도 더 좋은 구문을 제공합니다.

새로운 Http에 기본적인 사용 방법에 대한 예시입니다.

use Illuminate\Support\Facades\Http;

$response = Http::post('url', [
    'name' => 'Taylor',
]);

echo $response['foo'];

$response->body()
$response->json()
$response->status()
$response->ok()
$response->successful() (>= 200 &amp;&amp; < 300)
$response->serverError()
$response->clientError()

Taylor의 PR내용을 보면, 이 패키지는 완전 새로운 클라이언트가 아닌 Guzzle 윗 레이어에서 UX/DX를 좀더 펼리하게 해주는 역할을 한다고 합니다.

이 패키지는 복잡하고 많은 내용이 추가되는것이 아닙니다. 그리고 만약 더 많은 것들이 필요하다면 Guzzle를 직접 사용할 수 있습니다.

새로운 HTTP 패키지에 대한 내용을 알고 싶으면 pull request를 확인해보세요.


새로운 Http 패키지 분석 (?)

Taylor의 PR를 보면 “Guzzle의 모든 기능을 이 API를 통해 제공하려는게 아니다.”라고 적혀있는걸 보면 말 그대로 TEST나 간단한 Http request정도만 지원하고 나머지는 Guzzle를 직접 적으로 사용하라는것같다.

더 많은 예시

새로운 Http 패키지의 기본적인 content-typeapplication/json이라고 한다.
하지만 아래와 같은 코드로 쉽게 form-urlencoded요청을 보낼 수 있다.

$response = Http::asForm()->post('url', [
    'name' => 'Taylor',
]);

그리고 multi-part타입으로 파일을 보낼 수도 있다.

$response = Http::attach('name', 'stream/contents', 'filename.txt')->post('url');

해더 / 인증정보 (Authentication)

해더는 withHeaders 메소드로 보낼 수 있다.

$response = Http::withHeaders(['X-Foo' => 'bar'])->post('url', [
    'name' => 'Taylor',
]);

인증 / bearer 과 같은 토큰 정보는 withToken메소드로 보낼 수 있다.

$response = Http::withToken('token')->post('url', [
    'name' => 'Taylor',
]);

Basic Authenication은 withBasicAuth메소드로 보낼 수 있다.

$response = Http::withBasicAuth('username', 'password')->post('url', [
    'name' => 'Taylor',
]);

에러

ZTTP와 같이 기본적으로 서버, 클라이언트 에러를 출력하지 않는다고 한다.
하지만 만약 HTTP응답이 실패했을때 $response->throw()를 통해 예외 처리를 할 수 있다.

$response = Http::post(...);

if (! $response->successful()) {
    $response->throw();
}

Testing / Faking

Http::fake()를 지원하며, 기본적으로 비여있는 200응답을 한다.

Http::fake();

$response = Http::post('url');

Http::fake()는 인자로 Array를 받으며, 패턴이 일치하는 경우는 Http::response를 통해 응답을 만들어 낼 수 있으며, 만약 일치되는 패턴이 없다면, 실제로 실행된다.

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
]);

만약 모든 패턴에 대해서 실행되는것을 막고 싶다면 아래와 같이 작성할 수 있다.

Http::fake([
    'github.com/*' => Http::response([1, 2, 3], 200, ['Headers']),
    '*' => Http::response('', 200),
]);

연속적인 응답은 Http::sequentce를 이용해 지정된 순서에 맞게 응답이 출력된다.

Http::fake([
    '*' => Http::sequence([Http::response('foo', 200), Http::response('bar', 200)]),
]);

마지막으로 Http::fake()는 클로저를 지원한다.

Http::fake(function ($request) {
    return Http::response('foo', 200);
});

잡-설

잘 모르는 영어와 개발 코드를 보니 복잡하네요..
새로운 패키지가 들어오는건 좋지만 아직도 배울게 한참많은데
배워야 될것들이 계속 늘어나는걸 보면,, ㅜㅜ

[Laravel 7] Custom mutators

TL; TR

Laravel 7.x에선 모델의 Property인 $casts에 커스텀된 클래스를 전달해줄 수 있다.[링크]

기존의 Mutators

Laravel 7.0의 공개 일자는 2020년 2월로 현재 얼마 남지 않은 상황에 laravel.com/doc/master에 차근차근 새로운 기능들에 대한 문서들도 업데이트 되는 것 같다.

이번에 getVarNameAttribute()setVarNameAttribute($value) 를 사용하면서 mutator에 대한 커스텀은 안될까 하여 찾아보다가 master문서에 custom mutators 항목이 추가된 것을 발견하고 곧 7.0이 릴리즈 될테니 테스트겸 시도해보았다.

Laravel 7 dev 버전에 대한 설치는
laravel new laravel7 --dev 또는
composer create-project --prefer-dist laravel/laravel:7.x-dev laravel7 으로 설치 할 수 있다.

$ cd laravel7
$ php artisan --version
Laravel Framework 7.x-dev

위와같이 artisan 명령어로 버전을 확인할 수 있다.
과거엔 welcome 템플릿에 laravel 7.0 (DEV) 🚀 라고 적혀있엇는데 아마 곧 공개를 앞으도 바뀐것같다.

++ User 테이블로 Custom mutator를 테스트 해보려고 하는데 laravel/ui 가 설치되지 않는다. 찾아보니 laravel7.x 부턴 laravel/ui에 login, register 등의 컨트롤러가 포함되어 배포되기에 laravel/ui 2.x를 설치해 줘야한다.
composer requrie laravel/ui:2.x-dev --dev

기존에 mutators의 경우 array, datetime 등의 일부만 작동을 했고 만약 커스텀하게 짜기위해선 아래와 같이 해당 모델에 get, set~~Attribute 메소드를 짜줘야했다.

// App/User

public function getNameAttribute()
{
    return "my name is {$this->name}";
}

public function setNameAttribute($value)
{
    $this->name = ucfirst($value);
}

하지만 여러 테이블에서 사용될 때 반복되고 모델에도 만약 수정이 필요할 때 모델별로 다 수정해줘야하는 번거로움이 생긴다 (trait를 써도되지만 일단넘어가자..)

이번 Laravel 7.x 에선 Illuminate\Contracts\Database\Eloquent\CastsAttributes 클래스를 사용해서 datetime, array와 같인 $casts 프로퍼티에 지정할수있다.

App\Casts\Json

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return json_decode($value, true);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}

App\User

<?php

namespace App;

use App\Casts\Json;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'options' => Json::class,
    ];
}

위와 같이 CastsAttributes 클래스를 받고 해당 클래스를 $casts로 전달해주면 자동으로 맵핑해준다.

지금은 casts를 따로 생성하는 artisan명령어는 없는것같지만, laravel 7이 릴리즈되면 추가되지 않을까싶다.

PHPUnit에서 Laravel 에러 메시지 출력 패키지(Collision) 사용

원글 Collision PHPUnit Listener With Laravel

Collision‘는 Laravel에서 아래 이미지처럼 상세한 오류 내역을 출력해주는 패키지이다.

View image on Twitter
[사진] Nuno maduro 트위터

Laravel로 개발하면서 정말 좋은 기능임을 알고있었지만, phpunit과 같은 외부 테스트 도구들을 통해 테스트할 때 오류가 발생해도 아래 이미치 처럼 단순히 Exception stack만 보여줘 ‘Collision’ 처럼 자세한 내용을 제공 받지 못해 아쉬웠던 적이 더러 있었다.

하지만 작년 12월 31일 Collision 패키지 개발자가 트위터를 통해 PHPUnit adapter를 언급하면서 간단한 설정만으로 phpunit에서도 Collision 화면을 볼 수 있게 되었다.

기존 phpunit은 아래 이미지처럼 Exception이 일어나면 단순히 Exception stack만을 표시 했지만,

[laravel-new] phpunit 에러 메시지

phpunit.xml파일에 Collision 리스너를 추가한다.

<listeners>
    <listener class="NunoMaduro\Collision\Adapters\Phpunit\Listener" />
</listeners>
[laravel-new] Listener 추가 후 phpunit 에러 메시지

그러면 위와 같이 phpnuit에서도 깔끔하게 출력되며 좀 더 나은 디버깅 환경을 제공해준다.

[PHP] 폴더 내에 모든 파일을 autoload하는 방법

라라벨에서 custom helper 함수를 추가할 땐 “app/helpers/helper.php”와 같은 파일을 만들고 아래 json과 같이 그 파일을 composer.json에 “files”에 추가해주어야 한다.

"autoload": {
    "files": [
        "app/helper/helpers.php"
    ],
    "classmap": [
        "database/seeds",
        "database/factories"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
    }
},

다만 하나의 file을 추가할 때는 괜찮아 보이지만 만약 추가해야하는 custom helper가 많아질 수록 관리도 힘들고 일일히 추가해야 하는 번거로움도 있다.

이럴때 폴더 단위로 app/Helpers/* 와 같이 로드되면 좋겠지만 composer의 files는 단일 파일만을 지원하며 * 와같이 여러 파일을 불러오는 기능은 존재하지 않는다.
(다만 class 와 관련되어선 사용할 수 있는것같다, 아마 namespace를 이용하지 않을까…?)

이럴때 app/helper/include.php 파일을 생성한 뒤 아래와 같이 작성해 준 뒤,

<?php
$files = glob(__DIR__ . '/*.php');
if ($files === false) {
    throw new RuntimeException("Failed to glob for function files");
}
foreach ($files as $file) {
    require_once $file;
}
unset($file);
unset($files);

include.php 파일을 composer.json에 추가해준다.

"autoload": {
    "files": [
        "app/helper/include.php"
    ],
    "classmap": [
        "database/seeds",
        "database/factories"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
    }
},

이후 composer dump-autoload 명령어를 통해 로드 해주면 app/helpers 폴더 안에 있는 모든 php 파일들에서 선언된 함수, 클래스들을 사용할 수 있다.