[PHP:rfc] 더 짧은 속성 구문

원문: https://wiki.php.net/rfc/shorter_attribute_syntax

PHP 8.0에선 많은 내용들이 추가되면서, rfc들도 많이 요청되면서 다양한 토론들이 이루어지고 있습니다.
해당 rfc는 2020/06/03 23:24에 최초로 생성된 rfc이며, 내부 검토를 마치고 토론중에 있습니다.

주장도 함리적이고, 내용들에 대해서 자세한 예시까지 해줘서 해당 rfc를 간단한게 정리해보았습니다.


PHP RFC: Shorter Attribute Syntax


Introduction

우리는 PHP 8.0에서 속성 구문을 <<Attr>> 대신 @@Attr을 사용할 것을 제안합니다.

저번달 초, Attributes v2는 PHP 8.0적용되는 것이 승인되었으며, 투표 동안 <<Attr>>@:Attr의 구문중에서 <<Attr>>구문이 3/4에게 선택 되었습니다.
하지만, 이 구문은 몇 가지 주목할만한 단점이 있습니다.

1. 장황함 (Verbosity)

아래의 RFC 예시에선 속성의 이름 보다 시프트 토큰(<<>>)에 사용되는 문자가 더 많습니다.

<<Jit>>
function foo() {}

특히 하나 이상의 속성이 필요할땐 이 구문은 한눈에 보기 어려울 수 있습니다.
이를 완화 하기위해  속성 수정 RFC에선 여러개의 속성에서 하나의 시프트 토큰을 사용하는 방법을 제안 했습니다.

<<Attr1("foo"),
  Attr2("bar")>>
public function test() {}

하지만, 이 방법은 다른 문제가 있습니다. 속성을 추가, 삭제하려면 여러줄을 수정되어 diff에 노이즈가 생기게 됩니다.
다만, 이 제한은 후행 쉼표를 허용해 다음과 같이 작성 하여 해결 할 수 있습니다.

<<
  Attr1("foo"),
  Attr2("bar"),
>>
public function test() {}

하지만, 스프트 토큰에 두개의 추가 행이 필요하며, 속성끼리 서로 이동할 때에 여러 줄이 수정되어지게 됩니다.

2. 중접 된 속성의 부재 (Lack of nested attributes)

중접된 주석은 Doctrine에서 많이 이용 되고 있으며, 여기서 그 예를 볼 수 있다.

@JoinTable(
    name="User_Group",
    joinColumns={@JoinColumn(name="User_id", referencedColumnName="id")},
    inverseJoinColumns={@JoinColumn(name="Group_id", referencedColumnName="id")}
)

<<>>구문(및 잠재적 속성 그룹화)으로 속성을 중접하는 방법은 새로운 Attr 표현식을 허용하는 것입니다.
하지만, 구현하기 쉽지 않으며, 혼란을 야기할수 있습니다. (예시로 new Foo()는 허용되지만, Foo::create()는 작동되지 않습니다).
결국 이것은 const 표현식에 많은 변경이 필요하기에 구현 중 포기되었습니다.

3. 제너릭과의 혼동 (Confusion with generics)

속성과 제너릭의 구문간에 기술적으로 충돌은 없지만, PHP에서 제너릭이 지원되면 속성과 제너릭이 사용되는 위치를 한눈에 파악하기 더 여러울 수 있습니다.

4. 쉬프트 연산자와 const 인수와의 혼동 (Confusion with shift operators and const arguments)

시프트 토큰을 재사용 하면, 상수 표현식인지, 멀티 속성인지, 또는 쉬프트 토큰인지 식별하기 어려울 수 있습니다.

<<Bar(2 * (3 + 3)>>Baz, (4 + 5) * 2)>>
function foo() {}

물론 이 이런 상황에서의 쉬프트 토큰은 거의 사용되지 않으므로 마니어너한 문제입니다만,
만약 그룹화 된 속성 제한이 승인되면, 쉼표로 구분된 속성과 속성인수간의 유사한 혼동이 있을 수 있습니다.

<<Attr1(2 * 3 + 3), Bar(4 + 5 * 2)>>
<<Attr2(2 * (3 + 3), Baz, (4 + 5) * 2)>>
function foo() {}

5. 다른 언어와 유사하지 않음 (Dissimilarity to other languages)

대부분의 C 패밀리 언어들은 속성 구문에 [Attr] 또는 @Attr 를 사용하며ㅡ, 4개가 아닌 1~2개의 문자만 필요합니다. (아래의 비교 참고)


Proposal

PHP8.0에서 <<Attr>> 구문 대신에 @@Attr구문을 사용합시다.

그렇게하면 위의 문제가 해결되며, 속성이 있는 대부분읜 다른 언어와 비교하여 절반 정도의 문자만 필요하므로 장황함이 줄어들게 됩니다.

@@Jit
function foo() {}

@@구문은 충접된 속성과 충돌 하지않아, 원하는 경우 나중에 지원을 추가하는 것이 간단해집니다. (특이 케이스나 const 표션식을 변경할 필요없이)

@@JoinTable(
    "User_Group",
    @@JoinColumn("User_id", "id"),
    @@JoinColumn("Group_id", "id"),
)
private $groups;

더 짧은 구문은 제너릭 또는 스프트 토큰과 달리 속성이 사용되는 위치를 쉽게 알 수 있어 코드 가독성을 향상 시킬 수 있습니다.

@@Attr1("foo")
@@Attr2("bar") // 이 라인은 다른줄과 독립적으로 추가, 삭제할 수 있습니다.
public function test() {}

Discussion

왜 “@:” 구문을 반대 합니까?

우선 “@;”와 같은 실수로 오타가 발생하기 쉽다는 것입니다.
일부 사람들의 경우 비대칭적인 문법을 싫어 하기 때문이기에 PHP의 토큰들과 잘 맞지 않기 때문입니다.
@@문법은 위 두가지 사항을 모두 해결합니다.

왜 키워드로 대신 사용하지 않습니까?

기호 대신 키워드를 사용하는것은 좋습니다.

attribute Foo();
function myFunc() {}

하지만, 제안의 목적은 덜 일반적이고 다른 언어들에서 공통적으로 사용되는 속성 구문과 더 잘맞는 구문에 도달 하는 것입니다.
키워드를 사용하게되면 이러한 목표를 충족하지 못하게됩니다.

구문의 선택은 우리가 주관이 아닌 익숙한것으로 해야하지 않습니까?

어느정도 동의합니다. 하지만 이 RFC가 간략화 하려는 짧은 구문을 선호하는 객관적인 이유도 있을 것입니다.

다른 언어들과의 비교

속성이 있는 다른 언어들은 구문에 [Attr] 혹은 @Attr을 변형하여 사용합니다.
Hack은 <<Attr>>을 사용하는 유일한 언어이지만 PHP와의 호환을 지향하지 않기 때문에 @Attr로 이전 하는 것 같습니다.

  • C#: [Attr] 1
  • C++: [[attr]] 2
  • Hack: <<Attr>> 3 (하지만, @Attr로 변경 중에 있습니다.) 4
  • Java: @Attr 5
  • Kotlin: @Attr 6
  • Python: @attr 7
  • Rust: #[attr] 8
  • Swift: @attr 9
  • TypeScript/JS: @Attr 10

Backward Incompatible Changes (이전 버전과 호환되지 않는 변경점)

이론적으로 BC(하위호환) 오류가 적습니다.
이후 다중 오차 억제 연산자들은 추가적인 영향이 추가될 수 있다. (예: @@@@@really_suppress_me()). (의역)
하지만 이렇게 사용하는 것은 유용하지 않으며, 어느 곳에서도 사용되지 않을 것이다.


Vote

PHP 8.0에서 속성 구문으로 제안된 @@ 를 사용 하시겠습니까? Yes/No [현재(2020/06/04)는 토론중입니다]


References


내용이 재미 있어보이고 좋은 의견이여서 번역해 봤습니다만,
개인적인 의견으론 Hack처럼 @ 연산자를 버리고 @Attr로 가는것도 괜찮아 보이는것같습니다. ㅎㅎ

간단히 공부하는 OAuth

이번 프로젝트 중에 OAuth를 구현하는 프로젝트가 있어 늘 궁금했지만, 그리고 늘 해야한다고 생각만 했던 OAuth에 대해 공부한 내용을 최대한 간단하고 간결하게 이야기해보려고 한다.

OAuth 1.0 ? 2.0?

왜 다들 2.0 만 사용하는지 궁금했는데, 간단히 요약하면,
2.0은 Https에게 보안을 전가 하고 인증에만 중심을 두겠다는거고,
1.0은 http던 https던 ftp던 뭐던 상관없이 (독립적으로) 인증을 하기위해 직접 보안을 신경써서 전송하겠다는 거다.

그 외의 명칭이 다르거나 다소 차이점은 있지만 가장큰 차이점은 위에서 말한 전송 방식에 대한 보안을 어떻게 신경쓰느냐 인 것 같다.

OAuth2.0 그리고 지옥으로 가는 길 과 같은 2.0은 위험하다! 라는 이야기도 있지만, 그러면 구글이 아직 까지 쓰고 있지 않을테니 여기서 가장 위험하다고 말해지는 https가 아닌 http를 사용할 경우의 위험만 신경쓰고 OAuth2.0는 https 상에서 통신 한다는 것만을 잊지 말자.

통신 순서

늘 어느 글을 가나 나오는 통신 순서다.

OAuth2 Overview | OAuth2

라고 하는데 솔직히 이것만 봐도 그리고 설명을 쭉 봐도 이해가 잘 되지 않는다.
그래서 그냥 내 나름 대로 정리해 보려고 한다.

목표와 명칭 정리

나는 내가 운영하는 사이트에 페이스북 로그인을 넣으려고 한다.
그럴때 아래와 같은 명칭을 가진다.

Resource Owner: 로그인 하는 사람이다.
Client: 내가 운영하는 사이트다.
Resource Server: 페이스북이다.
Authorization Server: 인증서버인데. 그냥 Resource Server와 통합되있다고 생각하자.

1. OAuth 준비

먼저 난 페이스북 개발자 사이트에 가서 Client id와 Client secret key를 발급 받는다.

2. 로그인 페이지에 페이스북 로그인 버튼을 추가한다.

이제 처음 들어오는 유저가 페이스북 로그인을 클릭하고,

최초 접속이니 내 서버의 Client id와 함께 해당 유저를 페이스북 OAuth 인증 페이지로 리다이렉트 한다.

늘 보던 이런거

확인을 누르면 callback URL으로 임시 패스워드[Authorization code]를 전달한다.

3. 내 서버에서 Access Token 발급 요청

서버는 임시 패스워드와[Authorization code]
내 서버의 client id, secret key를 가지고 Access Token을 발급 요청을 한다.
(혹은 Refresh token도 함께 받는다) 그리고 저장한다.

4. 내 서버에서 Access Token을 사용해 사용자 데이터에 접근한다.

발급 받은 Access Token을 사용해 사용자에 데이터에 접근할 수 있다.
그 정보로 로그인 내 서버에 로그인 시켜주면된다.

다만, 일정 시간이 지나면 Access Token이 만료되기에 Refresh Token을 사용하여 재발급 받는다.

이때 Refresh Token은 영구적으로 안별할 수 도 있고, 재발급 받을 때 마다 Refresh Token도 재발급 될 수 있다. 이는 각 사이트 정책을 따른다.

+α 이미 정보가 저장 되있으면?

그럼 위 2번에서 바로 내 서버의 callback url로 임시 패스워드가[Authorization code] 보내지고 Access Token을 가지고 혹은 갱신해서 로그인 정보를 가지고 올 수 있다.

즉 확인 버튼 누르는 과정만 없어졋다고 생각 해도 된다.


여튼 여러가지 설명이 있는데 내가 이해한 방식은 위와 같은 방식이고,
OAuth를 이해 하는데 도움이 되었으면 좋겠다.

[phpunit] Skip Test with message

PHPunit을 사용해 테스트를 하다보면 추후 기능 구현을 위해 남겨 두었다가 개발을 위해 잠시 테스트를 스킵하고 싶은데,
이에 대한 걸 단순히 주석 처리나, ignore시켜서 하기에는 나중에 나 스스로를 못믿기도 하고(…)
테스트 할때마다 이를 표시하고 싶은데, risk 상태로 두자니 찝찝하고 여러모로 어떻게 처리해야할지 모르는 상황에서 markTestSkipped메소드를 찾게 되었다.

Mark Test Skipped

이 메소드는 이름 그대로 해당 test를 넘겨주는 역할을 한다. 다만, 테스트를 아에 건너 뛰는것이 아닌 해당 테스트가 스킵되었고 어떤 내용으로 스킵되었는지를 표시해준다.

// Test codes ...
    public function testSomeThing(): void
    {
        $this->markTestSkipped('this test skipped because i want');
        // ...
    }
// Test codes ...

위처럼 markTestSkipped는 문자열을 인수로 받는다. skip의 s 로 표시된다.

PHPUnit 8.5.4 by Sebastian Bergmann and contributors.

.S                                                  2 / 2 (100%)

Time: 37.1 seconds, Memory: 28.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 1, Skipped: 1.

markTestIncomplete 와의 차이점

또한 phpunit에는 markTestIncomplete메소드가 있어 해당 테스트가 완료되지 않았다는 것을 표기해 줄 수 있다. 위에 상황을 봣을 땐 이 메소드가 더 어울리는 것같지만, markTestSkippedartisan test를 이용할 때 다른 결과를 보여준다.

위처럼 markTestSkipped는 해당 인수는 아래와 같이 테스트 결과와 함께 표시되며, 인자로 전달된 메시지를 출력해준다. 이를 이용해 왜 해당 테스트가 skip되었는지를 명확하게 표시 해 줄 수 있다.

silnex@localhost:~/test$ artisan test

   WARN  Tests\Feature\MyTest
  ✓ others
  s some thing → this test skipped because i want

  Tests:  1 skipped, 1 passed
  Time:   0.10s
silnex@localhost:~/test$ artisan test

   WARN  Tests\Feature\MyTest
  ✓ others
  i some thing

  Tests:  1 incompleted, 1 passed
  Time:   0.11s

예를 들어 config(‘range’) 의 값이 10 미만일 때 만 skip하고 싶고 왜 스킵되었는 표시해주고 싶을때 사용할수 있다.

[Laravel] Route와 Conroller에서 id를 inject할 때 한 실수

훈련소를 다녀와서 코드를 보고 머리가 돌아가는 속도가 떨어졋다지만 이 정도 일줄은 몰라서 2시간 동안 해멘건데 적어둡니다.

Laravel에선 Route 을 할때 slug를 설정해 컨트롤러에 Model을 넣어줄수 있다. (DI.. 맞나?)
암튼 거기서 실수한 썰을 짦게 남겨 놓습니다.

<?php //web.php
Route::get('/korea/{korea}', 'KoreaController@show');
<?php //KoreaController.php

class KoreaController extender Controller
{
    // ...
    public function show(Korea $korae)
    {
        return $korae->toJson();
    }
    // ...
}

죽어라 /korea/1 로 접근 해도 안되어 미치는 줄 알았는데,
KoreaControllershow메서드를 자세히 보면 ‘korea’가 아니라 ‘korae’ 였다.

즉, Route에서 설정한 {korea}와 Controller에서 입력받는 ‘korae’가 다르기 때문에 ‘Korea’모델을 불러올 수 없었던 거였다.

예ㅖㅖ전에 튜토리얼에서 본 것 같은데 까먹고 있다가 3~4시간 헤메다 겨우 찾았다..

그누보드 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의 함수를 찾는 것을 확인 할 수 있다.