Search code examples
phplaravelckeditor

How do I upload an image via CKEditor version 4 in this Laravel 8 app?


I have been working on a blogging application in Laravel 8.

I use CKEditor 4 in the form tasked with adding a new article.

I web.php I have added this route:

Route::post('/ckupload', [ArticleController::class, 'ckupload'])->name('dashboard.articles.ckupload');

The form view:

<form method="POST" action="{{ route('dashboard.articles.add') }}" enctype="multipart/form-data" novalidate>
    @csrf
    <div class="row mb-2">
            <label for="title" class="col-md-12">{{ __('Title') }}</label>

            <div class="col-md-12 @error('title') has-error @enderror">
                    <input id="title" type="text" placeholder="Title" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ old('title') }}" autocomplete="title" autofocus>

                    @error('title')
                            <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                            </span>
                    @enderror
            </div>
    </div>

    <div class="row mb-2">
            <label for="short_description" class="col-md-12">{{ __('Short description') }}</label>

            <div class="col-md-12 @error('short_description') has-error @enderror">
                    <input id="short_description" type="text" placeholder="Short description" class="form-control @error('short_description') is-invalid @enderror" name="short_description" value="{{ old('short_description') }}" autocomplete="short_description" autofocus>

                    @error('short_description')
                            <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                            </span>
                    @enderror
            </div>
    </div>

    <div class="row mb-2">
        <label for="category" class="col-md-12">{{ __('Category') }}</label>

        <div class="col-md-12 @error('category_id') has-error @enderror">

            <select name="category_id" id="category" class="form-control @error('category_id') is-invalid @enderror">
                <option value="0">Pick a category</option>
                @foreach($categories as $category)
                    <option value="{{ $category->id }}">{{ $category->name }}</option>
                @endforeach
            </select>


                @error('category_id')
                        <span class="invalid-feedback" role="alert">
                                <strong>{{ $message }}</strong>
                        </span>
                @enderror
        </div>
    </div>

    <div class="row mb-2">
        <div class="col-md-12 d-flex align-items-center switch-toggle">
                <p class="mb-0 me-3">Featured article?</p>
                <input class="mt-1" type="checkbox" id="featured" name="featured" {{ old('featured') ? 'checked' : '' }}>
                <label class="px-1" for="featured">{{ __('Toggle') }}</label>
        </div>
    </div>

    <div class="row mb-2">
        <label for="image" class="col-md-12">{{ __('Article image') }}</label>

        <div class="col-md-12 post-image @error('image') has-error @enderror">
            <input type="file" value="{{ old('image') }}" name="image" id="file" class="file-upload-btn">

            @error('image')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-2">
        <label for="content" class="col-md-12">{{ __('Content') }}</label>

        <div class="col-md-12 @error('content') has-error @enderror">

            <textarea name="content" id="content" class="form-control @error('content') is-invalid @enderror" placeholder="Content" cols="30" rows="6">{{ old('content') }}</textarea>

            @error('content')
                    <span class="invalid-feedback" role="alert">
                            <strong>{{ $message }}</strong>
                    </span>
            @enderror
        </div>
    </div>

    <div class="row mb-0">
            <div class="col-md-12">
                    <button type="submit" class="w-100 btn btn-primary">
                            {{ __('Save') }}
                    </button>
            </div>
    </div>
</form>

In the controller, I have:

namespace App\Http\Controllers\Dashboard;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use App\Models\ArticleCategory;
use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class ArticleController extends Controller
{
    
  private $rules = [
        'category_id' => 'required|exists:article_categories,id',
        'title' => 'required|string|max:190',
        'short_description' => 'required|string|max:190',
        'image' =>  'image|mimes:jpeg,png,jpg|max:2048',
        'content' => 'required|string'
    ];

    private $messages = [
        'category_id.required' => 'Please pick a category for the article',
        'title.required' => 'Please provide a title for the article',
        'short_description.required' => 'The article needs a short description',
        'short_description.max' => 'The short description field is too long',
        'content.required' => 'Please add content'
    ];

    public function ckupload(Request $request) {
      if ($request->hasFile('upload')) {
        $fileName = md5(time()) . Auth::user()->id . '.' . $request->image->extension();
        $request->image->move(public_path('images/articles'), $fileName);
        $CKEditorFuncNum = $request->input('CKEditorFuncNum');
        $url = asset('images/articles' . $fileName);
        $response = "<script>window.parent.CKEDITOR.tools.callFunction($CKEditorFuncNum, '$url')</script>";
        @header('Content-type: text/html; charset=utf-8');
        echo $response;
      }

      return false;
    }

    public function save(Request $request) {
        // Validate form (with custom messages)
        $validator = Validator::make($request->all(), $this->rules, $this->messages);

        if ($validator->fails()) {
            return redirect()->back()->withErrors($validator->errors())->withInput();
        }

        $fields = $validator->validated();

        // Upload article image

        if (isset($request->image)) {
            $imageName = md5(time()) . Auth::user()->id . '.' . $request->image->extension();
            $request->image->move(public_path('images/articles'), $imageName);
        }

        // If no image is uploaded, use default.jpg
        $fields['image'] = null == $request->image ? 'default.jpg' : $imageName;

       // Upload image via CKEDITOR
       $this->ckupload($request);

        // Turn the 'featured' field value into a tiny integer
        $fields['featured'] = $request->get('featured') == 'on' ? 1 : 0;

        // Data to be added
        $form_data = [
            'user_id' => Auth::user()->id,
            'category_id' => $fields['category_id'],
            'title' => $fields['title'],
            'slug' => Str::slug($fields['title'], '-'),
            'short_description' => $fields['short_description'],
            'content' => $fields['content'],
            'featured' => $fields['featured'],
            'image' => $fields['image']
        ];

        // Insert data in the 'articles' table
        $query = Article::create($form_data);

        if ($query) {
            return redirect()->route('dashboard.articles')->with('success', 'The article titled "' . $form_data['title'] . '" was added');
        } else {
            return redirect()->back()->with('error', 'Adding article failed');
        }
    }
}

I call CKEditor like this:

<script>
   CKEDITOR.replace('content', {
      filebrowserUploadUrl: '{{route('dashboard.articles.ckupload').'?_token='.csrf_token()}}',
      filebrowserUploadMethod: 'form'
    });

    CKEDITOR.on("instanceReady", function(event) {
      event.editor.on("beforeCommandExec", function(event) {
          // Show the paste dialog for the paste buttons and right-click paste
          if (event.data.name == "paste") {
              event.editor._.forcePasteDialog = true;
          }
      })
    });
</script>

The problem

When trying to upload an image via CKEditor, I get this error in the browser console:

Uncaught ReferenceError: browser is not defined

When I use the "Upload" tab of the editor's dialog (see image below), I get the error

Failed to load resource: the server responded with a status of 500 (Internal Server Error)

enter image description here

Questions

  1. What causes the described error?
  2. What is the most reliable way to fix the issue (besides upgrading CKEditor to version 5)?

Solution

  • I have fixed the issue by changing the ckupload method to:

    public function ckupload(Request $request)
      {
          if ($request->hasFile('upload')) {
            $originName = $request->file('upload')->getClientOriginalName();
            $fileName = pathinfo($originName, PATHINFO_FILENAME);
            $extension = $request->file('upload')->getClientOriginalExtension();
            $fileName = md5(time()) . Auth::user()->id . '.' . $extension;
            $path = public_path('images/articles');
            $fileName = $request->file('upload')->move($path, $fileName)->getFilename();
    
            $CKEditorFuncNum = $request->input('CKEditorFuncNum');
            $url = asset('images/articles/' . $fileName);
            $response = "<script>window.parent.CKEDITOR.tools.callFunction($CKEditorFuncNum, '$url')</script>";
    
            echo $response;
          }
    
          return false;
    }
    

    There is no need to call this method within the save() method.