Open zealouskestrel opened 3 years ago
You need to write the JS uploader and then the Django view to handle the upload yourself. Here's what I did to achieve this where I use Django Storages as the backend but I use default_storage to write the file to the backend so everything should work for you with the exception of the full URL to the uploaded file.
Step 1. Ensure the the these two keys are set within the TINYMCE_DEFAULT_CONFIG
"images_upload_url": "/tinymce/upload",
"images_upload_handler": "tinymce_image_upload_handler"
The first is the path to which you would like the javascript to make the POST. The 2nd is the name of the javascript function to handle the upload
Step 2 Because of CSRF you need to be able to read the token. In order to do this I use the js-cookie lib mentioned within the Django docs
Add the below into your settings.py to tell it yo include two additional JS file. I'm using the jsdelivr CDN hosted version for the above package
TINYMCE_EXTRA_MEDIA = {
'css': {
'all': [
],
},
'js': [
"https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js",
"admin/js/tinymce-upload.js",
],
}
The second file is holds the custom JS file upload handler I will post below.
Step 3. Here are the contents of the tinymce-upload.js file I created. Note it uses cookie-js to read the value of the CSRF token and add it to the headers of the AJAX POST. I also hardset the URL path to post to as the same value as the one I add in the settings.py. You may be able to read it from somewhere but I haven't looked into that yet.
function tinymce_image_upload_handler (blobInfo, success, failure, progress) {
let xhr, formData;
xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', '/tinymce/upload');
xhr.setRequestHeader('X-CSRFToken', Cookies.get("csrftoken"));
xhr.upload.onprogress = function (e) {
progress(e.loaded / e.total * 100);
};
xhr.onload = function() {
let json;
if (xhr.status === 403) {
failure('HTTP Error: ' + xhr.status, { remove: true });
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
failure('HTTP Error: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
if (!json || typeof json.location != 'string') {
failure('Invalid JSON: ' + xhr.responseText);
return;
}
success(json.location);
};
xhr.onerror = function () {
failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
};
formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
xhr.send(formData);
}
Step 4 Write the view to handle the upload. In my case I want to use a subfolder of media called tinymce with a uuid4 named folder to then hold the file in to avoid name clashes.
The key part is to return a JSONResponse with the location of the image you just uploaded.
In my case I am using Django Storages to store files on S3 but I serve them from AWS Cloudfront using a custom domain that I have set within settings.py so I append that to the start
def tinymce_upload(request):
"""Uplaod file to S3 and return the location"""
file = request.FILES.get('file')
filename = f"tinymce/{uuid4()}/{str(file)}"
with default_storage.open(filename, "wb") as f:
f.write(file.read())
return JsonResponse({"location": f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/media/{filename}"})
Now I can upload images using the TinvMCE plugin ensuring that CSRF checks are made
Althought its unsecure, you could exempt the view images_upload_url from csrf token. Take a look at this: https://docs.djangoproject.com/en/4.1/howto/csrf/#disabling-csrf-protection-for-just-a-few-views
I did not test any of this this but hopefully someone could make a PR from this.
When using Django's CsrfViewMiddleware to include a CSRF token in your Django form to protect it from Cross-Site Request Forgery attacks, it can be tricky to perform file uploads when the user wants to upload images or other files that do not have an appropriate HTML form. Here's an alternative approach to include the CSRF token in the upload request when using django-tinymce:
function tinymce_upload_handler(file, success, failure) {
var csrftoken = getCookie('csrftoken');
var xhr, formData;
xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
success(JSON.parse(xhr.responseText).location);
} else {
failure('HTTP Error: ' + xhr.status);
}
}
};
formData = new FormData();
formData.append('file', file);
formData.append('csrfmiddlewaretoken', csrftoken);
xhr.open('POST', '/upload-handler/');
xhr.setRequestHeader('X-CSRFToken', csrftoken);
xhr.send(formData);
}
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
from django.http import JsonResponse
def upload_image(request):
if request.method == 'POST':
image_file = request.FILES.get('file')
# handle uploaded file
return JsonResponse({'success': True})
return JsonResponse({'success': False})
file_picker_callback: function(callback, value, meta) {
var input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*'); // Only accept image files
input.onchange = function() {
var file = this.files[0];
var reader = new FileReader();
reader.onload = function () {
// Build the request to send to the server
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload/file/', true);
xhr.setRequestHeader("X-CSRFToken", getCookie("csrftoken")); // Include the CSRF token
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
if (!json.error) {
callback(json.location); // Return the URL of the uploaded image
} else {
alert(json.error);
}
}
};
In your Django URLconf, add the following URL pattern:
from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt
from tinymce.views import handle_upload
urlpatterns = [
url(r'^tinymce/upload/$', csrf_exempt(handle_upload), name='tinymce_upload'),
]
In this example, we're adding a URL pattern that maps the URL /tinymce/upload/ to the handle_upload view provided by Django-tinymce. We're also using the csrf_exempt decorator to disable CSRF protection for this view.
In your TinyMCE configuration, add the following options:
TINYMCE_DEFAULT_CONFIG = {
'image_upload_url': reverse('tinymce_upload'),
'image_upload_method': 'POST',
}
In this example, we're setting the image_upload_url option to the URL pattern we defined earlier (reverse('tinymce_upload')), and the image_upload_method option to 'POST'.
Note that by using csrf_exempt, we're disabling CSRF protection for the handle_upload view. If you want to enable CSRF protection for this view, you'll need to use a different approach, such as including the CSRF token in the upload request.
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
@csrf_exempt
def upload_file(request):
if request.method == 'POST':
# Here you can handle the uploaded file and store it somewhere
# Make sure to return a JSON response with the URL of the uploaded file
# For example
return JsonResponse({'location': '/path/to/uploaded/file'})
In your tiny_mce_init.js file, update the file_picker_callback option to use the tinymce_upload_handler function we defined earlier and set the upload_url option to the URL of the file upload view you just created. Here's an example:
tinymce.init({
file_picker_callback: function(callback, value, meta) {
tinymce_upload_handler(callback, value, meta, '/upload-file/');
},
upload_url: '/upload-file/'
});
In your urls.py, add a URL pattern that will map to the view you just created. Here's an example:
from django.urls import path
from .views import image_upload_view
urlpatterns = [
path('upload-image/', image_upload_view, name='image_upload_view'),
]
In your template or JavaScript file, use the tinymce.init function to initialize TinyMCE with a custom image_upload_url that corresponds to the URL pattern you just added to your urls.py.
tinymce.init({
selector: '#mytextarea',
plugins: 'image',
toolbar: 'image',
image_upload_url: '/upload/image/',
image_upload_credentials: true,
file_picker_types: 'image',
file_picker_callback: function (callback, value, meta) {
tinymce.activeEditor.windowManager.openUrl({
url: '/file-picker/',
onMessage: function (api, message) {
callback(message.data);
}
});
}
});
Test your implementation by creating a new Post or Page in your site that contains an image. When you click the image button in TinyMCE's toolbar, you should see an "Upload" tab that allows you to select an image file and upload it to your Django application. Once the upload is complete, the image should be inserted into the text editor.
Hi there, I used @StevenMapes solution and tried to improve it a little bit. Thanks to him
settings.py
add images_upload_url
and file_picker_callback
:
TINYMCE_DEFAULT_CONFIG = {
"height": 490,
"promotion": False,
"menubar": False,
"width": "full",
"plugins": "advlist autolink lists link image media charmap preview anchor pagebreak directionality emoticons fullscreen table visualblocks code codesample",
"toolbar1": "fullscreen preview | undo redo | fontfamily fontsize | numlist bullist accordion | indent outdent ltr rtl | blocks",
"toolbar2": (
"pagebreak footnotes | bold italic underline strikethrough subscript superscript | formatpainter backcolor forecolor removeformat |"
"align lineheight | charmap emoticons link image media table codesample | code visualblocks"
),
# Add these two lines
"images_upload_url": "/tinymce/upload/",
"file_picker_callback": "filePickerCallback",
}
right after that add this
TINYMCE_EXTRA_MEDIA = {
"css": {
"all": [],
},
"js": [
"https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js",
# Path to the script
"admin/js/tinymce-upload.js", # change it accordingly
],
}
add the script (tinymce-upload.js
) with this content
Note: Don't forget to check the value of images_upload_url
must be the same xhr.open("POST", images_upload_url);
function filePickerCallback(cb, value, meta) {
var input = document.createElement("input");
input.setAttribute("type", "file");
if (meta.filetype == "image") {
input.setAttribute("accept", "image/*");
}
input.onchange = function () {
var file = this.files[0];
var formData = new FormData();
formData.append("file", file);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/tinymce/upload/"); // change this accordingly
xhr.setRequestHeader("X-CSRFToken", Cookies.get("csrftoken"));
xhr.onload = function () {
if (xhr.status < 200 || xhr.status >= 300) {
console.error("Upload failed with status: " + xhr.status);
return;
}
var json = JSON.parse(xhr.responseText);
if (!json || typeof json.location !== "string") {
console.error("Invalid JSON response: " + xhr.responseText);
return;
}
cb(json.location, { title: file.name });
};
xhr.onerror = function () {
console.error("Upload failed.");
};
xhr.send(formData);
};
input.click();
}
from core.models import File
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def tinymce_upload(request):
"""Upload file to AWS S3 and return the location."""
if request.method != "POST" or not request.FILES.get("file"):
return JsonResponse({"error": "Invalid request"}, status=400)
file = request.FILES["file"]
file_instance = File.objects.create(file=file)
return JsonResponse({"location": file_instance.file.url})
Note: Here I configured AWS
and django storages
and I also added a model called File
:
settings.py
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
AWS_SERVICE_NAME = os.getenv("AWS_SERVICE_NAME", "s3")
AWS_S3_FILE_OVERWRITE = False if os.getenv("AWS_S3_FILE_OVERWRITE") == "False" else True
AWS_LOCAL_STORAGE = BASE_DIR / os.getenv("AWS_LOCAL_STORAGE", "aws")
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
in one of your applications in models.py
class File(models.Model):
file = models.FileField()
def __str__(self):
return self.name
You can use default_storage
functionality and generate the url
yourself I preferred this solution to make it more flexible.
Hope it helps! ✌️
i wanna know how upload image i already set tinymce.init and the problem is the images_upload_url how i can upload them and it's require csrf too :( help meeee