2010년 3월 9일 화요일

FCKEditor 이미지 업로드 기능 구현하기

FCKEditor를 사용하는 Java 환경에서 이미지 업로드를 구현 하기 위해서는
FCKEditor Java 버전을 다운받아서 사용해야 한다.

WAS가 톰캣인 경우라면 별다른 수정없이 웹에 널리 알려진 방법대로 사용이 가능하나,
업로드 쪽을 직접 뜯어 고치고 싶다던지, 톰캣 환경이 아니라면 소스의 수정은 불가피하다.


우리가 지금 진행중인 프로젝트 역시 조금은 다른 환경에서 가동되므로,
FCKEditor 를 약간 수정 하되, FCKEditor에서 제공하는 디자인과 틀을 그대로 가져가면서
업로드 처리 부분만 제작했다.
다시 간단하게 말해서, FCKEditor 에서 요구하는 양식에 맞춰 주기만 하면 된다.

서버측 파일 업로드 프로그램이야 알아서 간단하게 개발 가능할 테지만
이를 FCKEditor 이미지 업로드 폼에서 작동 되게 하는건 조금의 설정이 필요!.

fckeditor 폴더의 editor -> dialog 폴더와 그아래 fck_image 폴더에 우리가 찾는 이미지 업로드 다이얼로그 html 폼과 js 파일이 존재 한다. (이는 FCKEditor 2.6.6 기준이다)

fck_image.js 파일을 열고
window.onload 이벤트에서 아래와 같은 부분을 찾아서 직접 만든 업로드 처리 서블릿을 명시한다.

    // Set the actual uploader URL.
    if ( FCKConfig.ImageUpload )
        GetE('frmUpload').action = "니업로드처리서블릿주소";

    dialog.SetAutoSize( true ) ;


여기 주소만 수정해줘도 거의 모든 설정은 끝난 것과 마찬가지
(이부분에서 설정이 중요하다. html 파일의 upload form 영역의 action 주소는 공백으로 놔둘것)

아래로 더 살펴보면 아래와 같은 두가지 메소드를 찾을 수 있다.


function OnUploadCompleted( errorNumber, fileUrl, fileName, customMsg )
{
    // Remove animation
    window.parent.Throbber.Hide() ;
    GetE( 'divUpload' ).style.display  = '' ;

    switch ( errorNumber )
    {
        case 0 :    // No errors
            alert( 'Your file has been successfully uploaded' ) ;
            break ;
        case 1 :    // Custom error
            alert( customMsg ) ;
            return ;
        case 101 :    // Custom warning
            alert( customMsg ) ;
            break ;
        case 201 :
            alert( 'A file with the same name is already available. The uploaded file has been renamed to "' + fileName + '"' ) ;
            break ;
        case 202 :
            alert( 'Invalid file type' ) ;
            return ;
        case 203 :
            alert( "Security error. You probably don't have enough permissions to upload. Please check your server." ) ;
            return ;
        case 500 :
            alert( 'The connector is disabled' ) ;
            break ;
        default :
            alert( 'Error on file upload. Error number: ' + errorNumber ) ;
            return ;
    }

    sActualBrowser = '' ;
    SetUrl( fileUrl ) ;
    GetE('frmUpload').reset() ;
}

var oUploadAllowedExtRegex    = new RegExp( FCKConfig.ImageUploadAllowedExtensions, 'i' ) ;
var oUploadDeniedExtRegex    = new RegExp( FCKConfig.ImageUploadDeniedExtensions, 'i' ) ;

function CheckUpload()
{
    var sFile = GetE('txtUploadFile').value ;
    if ( sFile.length == 0 )
    {
        alert( 'Please select a file to upload' ) ;
        return false ;
    }

    if ( ( FCKConfig.ImageUploadAllowedExtensions.length > 0 && !oUploadAllowedExtRegex.test( sFile ) ) ||
        ( FCKConfig.ImageUploadDeniedExtensions.length > 0 && oUploadDeniedExtRegex.test( sFile ) ) )
    {
        OnUploadCompleted( 202 ) ;
        return false ;
    }

    // Show animation
    window.parent.Throbber.Show( 100 ) ;
    GetE( 'divUpload' ).style.display  = 'none' ;

    return true ;
}


메소드 명만 봐도 아! 우리는 저것이 뭘 하는 이벤트겠구나 하는 감을 잡을 수 있다.
폼이 전송 되기전 onSubmit 이벤트 처리 메소드이며 하나는 전송이 완료되어 호출 되는 callback 메소드이다.

이 두가지 메소드는 뜯어 고칠 일이 전혀 없다.

파일 업로드를 처리하는 서블릿에서 파일 전송이 끝났다면
직접 OnUploadCompleted 메소드만 호출 해 주면 끝난다.

ex: 업로드를 처리하는 서블릿에서 스크립트 메소드 호출
    if(!item.isFormField() && item.getSize()>0) {
                       
                        String fieldName = item.getFieldName();
                        String fileName = item.getName();
                        String contentType = item.getContentType();
                        boolean isInMemory = item.isInMemory();
                        long sizeInBytes = item.getSize();
                       
                        File uploadedFile = new File(savePath,fileName);
                        try {
                            item.write(uploadedFile);
                            item.delete();
                           
                            String retVal="0";
                            String newName="";
                            String fileUrl= "http://"+request.getServletRequest().getServerName()+":"+request.getServletRequest().getServerPort()+"/common/uploads/"+fileName;
                            String errorMessage="";
                       
                            HttpServletResponse res = request.getServletResponse(true);
                            PrintWriter out = res.getWriter();
                            out.println("<script type=\"text/javascript\">");
                            out.println(" window.parent.OnUploadCompleted("+retVal+",'"+fileUrl+"','"+newName+"','"+errorMessage+"');");
                            out.println("</script>");
                            out.flush();
                            out.close();



끝...

혹시라도 위의 작업을 완료하고 이미지를 업로드 하는데 만약,
액세스 거부 에러 또는 권한 없다는 스크립트 에러가 나면 본문과 이 서블릿에서
document.domain 을 서로 맞춰주면 된다.


2010년 3월 8일 월요일

IE8에서 FCKEditor 액세스 거부 (Access Denied) 문제 해결하기


중요한 사항이라 기록 해 둠.
(전혀 안쓰는 블로그지만 가끔씩 생각나면 여기다. 쓰게 되는군아!!)


FCKEditor 2.x 버전에서 (혹은 그 이하도 마찬가지일지도 모름) IE8로 접근 할 시에
액세스가 거부되었습니다 라는 스크립트 에러 메세지로 글쓰기 영역이 생성되지 않는 문제가 있다.

해당 문제는 IE8 에서 일어나며 원인은 몇가지 있으나 우리에겐 원인이 중요 한 것이 아니라
해결책과 대안이 늘 중요하다 ㅡ.ㅡ


원인과 또 다른 해결 방법:
http://blog.naver.com/webdzang?Redirect=Log&logNo=50047871069


하지만 위 링크의 해결 방법은 우리의 상황에 맞지 않으며
직접적인 원인을 수정해야만 했다.
따라서 구글신에게 문의 해 본 결과, 아래와 같은 해결책을 찾았다.

소스에 IE8 일때의 경우를 추가:

 http://cksource.com/forums/viewtopic.php?f=6&t=13378&p=36503

 
이에 따라 수정 하려면 몇가지 고통이 필요 한데,
Compressed 된 fckeditorcode_ie.js 파일을 그대로 눈알빠지게 봐가며, 변수명 비교해가며, 수정 할 것인가
아니면 source 폴더의 스크립트 파일들을 수정하여 재 압축 할 것인가 라는 선택

선자는 고통이고 후자는 좀 귀찮다..
귀찮은건 고통보다 싫기에,  선자를 택하여 수정 했다.


fckeditorcode_ie.js 파일을 연다. (FCKEditor 2.6.6 버전 기준)

var FCKBrowserInfo={IsIE:/*@cc_on!@*/false,IsIE7:/*@cc_on!@*/false&&(parseInt(s.match(/msie (\d+)/)[1],10)>=7),IsIE6:/*@cc_on!@*/false&&(parseInt(s.match(/msie (\d+)/)[1],10)>=6),IsSafari:s.Contains(' applewebkit/'),IsOpera:!!window.opera,IsAIR:s.Contains(' adobeair/'),IsMac:s.Contains('macintosh')};

부분을 찾아

IsIE8: /*@cc_on!@*/false && ( parseInt( s.match( /msie (\d+)/ )[1],10)>=8) 를 추가한다.

var FCKBrowserInfo={IsIE:/*@cc_on!@*/false,IsIE8: /*@cc_on!@*/false && ( parseInt( s.match( /msie (\d+)/ )[1],10)>=8),IsIE7:/*@cc_on!@*/false&&(parseInt(s.match(/msie (\d+)/)[1],10)>=7),IsIE6:/*@cc_on!@*/false&&(parseInt(s.match(/msie (\d+)/)[1],10)>=6),IsSafari:s.Contains(' applewebkit/'),IsOpera:!!window.opera,IsAIR:s.Contains(' adobeair/'),IsMac:s.Contains('macintosh')};


fckeditor.html 파일을 열어 아래 부분을


// Save a reference to the default domain.
var FCK_ORIGINAL_DOMAIN ;

// Automatically detect the correct document.domain (#123).
(function()
{
    var d = FCK_ORIGINAL_DOMAIN = document.domain ;

    while ( true )
    {
        // Test if we can access a parent property.
        try
        {
            var test = window.parent.document.domain ;
            break ;
        }
        catch( e ) {}

        // Remove a domain part: www.mytest.example.com => mytest.example.com => example.com ...
        d = d.replace( /.*?(?:\.|$)/, '' ) ;

        if ( d.length == 0 )
            break ;        // It was not able to detect the domain.

        try
        {
            document.domain = d ;
        }
        catch (e)
        {
            break ;
        }
    }
})() ;

// Save a reference to the detected runtime domain.
var FCK_RUNTIME_DOMAIN = document.domain ;

var FCK_IS_CUSTOM_DOMAIN = ( FCK_ORIGINAL_DOMAIN != FCK_RUNTIME_DOMAIN ) ;

아래와 같이 바꾼다

var IsIE8 = /*@cc_on!@*/false && ( parseInt( navigator.userAgent.toLowerCase().match( /msie (\d+)/ )[1], 10 ) >= 8 );
// if IE8, setup a bunch of popups that use a domain we can touch....
var GLOBAL_POPUP_BUCKET_FCK_IE8 = [];
if(IsIE8){
      for(i=0; i<20; i++){
         GLOBAL_POPUP_BUCKET_FCK_IE8.push(window.createPopup());
      }
}


// Save a reference to the default domain.
var FCK_ORIGINAL_DOMAIN ;
var FCK_RUNTIME_DOMAIN ;
// Automatically detect the correct document.domain (#123).
(function()
{
   var d = FCK_ORIGINAL_DOMAIN = FCK_RUNTIME_DOMAIN = document.domain ;

   while ( true )
   {
      // Test if we can access a parent property.
      try
      {
         var test = window.parent.document.domain ;
         break ;
      }
      catch( e ) {}

      // Remove a domain part: www.mytest.example.com => mytest.example.com => example.com ...
      d = d.replace( /.*?(?:\.|$)/, '' ) ;

      if ( d.length == 0 )
         break ;      // It was not able to detect the domain.

      try
      {
         // Before setting document.domain, set it for all the popups we've created.  I hope this fucking works....
         for(i=0; i < GLOBAL_POPUP_BUCKET_FCK_IE8.length; i++){
            GLOBAL_POPUP_BUCKET_FCK_IE8[i].document.domain = d;
         }
        
         document.domain = d ;
      }
      catch (e)
      {
         break ;
      }
   }
})() ;

var FCK_RUNTIME_DOMAIN = document.domain ;

var FCK_IS_CUSTOM_DOMAIN = ( FCK_ORIGINAL_DOMAIN != FCK_RUNTIME_DOMAIN ) ;



다시 fckeditorcode_ie.js 를 열어 아래 부분에

var B; if (FCKBrowserInfo.IsIE){this._Popup=this._Window.createPopup();var C=this._Window.document;if (FCK_IS_CUSTOM_DOMAIN&&!FCKBrowserInfo.IsIE7){C.domain=FCK_ORIGINAL_DOMAIN;document.domain=FCK_ORIGINAL_DOMAIN;};B=this.Document=this._Popup.document;if (FCK_IS_CUSTOM_DOMAIN){B.domain=FCK_RUNTIME_DOMAIN;C.domain=FCK_RUNTIME_DOMAIN;document.domain=FCK_RUNTIME_DOMAIN;};FCK.IECleanup.AddItem(this,FCKPanel_Cleanup);}else{var D=this._IFrame=this._Window.document.createElement('iframe');

빨간색으로 표시된 브라우저 처리 부분을 추가해준다

var B;
if ( FCKBrowserInfo.IsIE8 )
    {
        this._Popup = GLOBAL_POPUP_BUCKET_FCK_IE8.pop();

         B = this.Document = this._Popup.document ;

         FCK.IECleanup.AddItem( this, FCKPanel_Cleanup ) ;
    }
    else if (FCKBrowserInfo.IsIE){this._Popup=this._Window.createPopup();var C=this._Window.document;if (FCK_IS_CUSTOM_DOMAIN&&!FCKBrowserInfo.IsIE7){C.domain=FCK_ORIGINAL_DOMAIN;document.domain=FCK_ORIGINAL_DOMAIN;};B=this.Document=this._Popup.document;if (FCK_IS_CUSTOM_DOMAIN){B.domain=FCK_RUNTIME_DOMAIN;C.domain=FCK_RUNTIME_DOMAIN;document.domain=FCK_RUNTIME_DOMAIN;};FCK.IECleanup.AddItem(this,FCKPanel_Cleanup);}else{var D=this._IFrame=this._Window.document.createElement('iframe');




끝 ㅡ.ㅡ;