Help with File Upload

Help with File Upload

djagercddjagercd Posts: 13Questions: 4Answers: 0

Hi all, I am using NodeJS, Express and DataTables.net Editor module. I have a page that allows me to add a general URL link and I would like to add an icon for the link. The current solution works perfectly to add a link, but the file upload is giving an issue.

Within my application I am using csurf for CSRF protection. In my page javascript I send the _csrf as part of ajax data.

// Ajax definition:
 editor = new $.fn.dataTable.Editor({
            ajax: {
                url: "/api/links",
                data: {
                    "_csrf" : "<%= csrfToken %>"
                }
            },

As mentioned this works fine for the data itself. For the file upload I have the following:

// File Upload section:
{
                    label: "Icon",
                    name: "links.icon",
                    type: "upload",
                    display: function (file_id) {
                        if (file_id !== 'null' && file_id) {
                            return '<img src="' + editor.file('icons', file_id).web_path + '"/>';
                        }
                    },
                    clearText: "Clear",
                    noImageText: 'No Image',
                    ajax: {
                        url: "/api/links",
                        data: {
                            "_csrf" : "<%= csrfToken %>"
                        }
                    },
                }

I have tried various options with no success.

Any ideas would be appreciated.

Full source code for the page:

<%- include('./includes/head.ejs') %>

</head>

<body>
<%- include('./includes/navigation.ejs') %>


<main class="main">
    <div class="container-fluid">
        <div id="list" class="row">
            <div class="col-lg-4">
                <button id="addLink" class="btn">Add Link</button>
            </div>
            <div class="col">
                <input id="searchText" class="active" placeholder="enter search...">
            </div>
        </div>
        <div class="container-fluid">
            <div id="cards" class="row">

            </div>
        </div>
    </div>
</main>
<%- include('./includes/end-scripts.ejs') %>

<script type="text/javascript">
    var editor;

    function createCard(data) {
        var id = data.DT_RowId;
        var html =
            '<div class="card col-sm-3" data-editor-id="' + id + '" id="' + id + '" data-name="' + data.links.name + '">' +
            '<div class="card-header">' +
            '<a class="hidden" data-editor-field="links.uuid">' + data.links.uuid + '</a>' +
            '<h5 class="card-title" data-editor-field="links.name">' + data.links.name + '</h5>' +
            '</div>' +
            '<div class="card-body">' +
            '<p class="card-text">' +
            '<p>Description:</p>' +
            '<p data-editor-field="links.description">' + data.links.description + '</p>' +
            '<p data-editor-field="links.icon">' + data.links.icon + '</p>' +
            '<a href="' + data.links.url + '" target="_blank"  data-editor-field="links.url">' + data.links.url + '</a>' +
            '</p>' +
            '</div>' +
            '<div class="card-footer text-center">' +
            '<button data-id="' + id + '" data-name="' + data.links.name + '" class="btn btn-sized edit">Edit</button>';

        //TODO
        //  Add code to only allow delete if user is admin


        //if (!data.menus.locked) {

        html = html + '<button data-id="' + id + '" data-name="' + data.links.name + '" class="btn btn-sized-danger delete">Delete</button>'

        // }

        html = html + '</div>' +
            '</div>';

        $(html).appendTo('#cards');

    }

    <!--$.ajaxSetup({-->
    <!--headers: {-->
    <!--'CSRFToken': "<%= csrfToken %>"-->
    <!--},-->
    <!--data: function(id) {-->
    <!--d._csrf = "<%= csrfToken %>"-->
    <!--}-->
    <!--});-->

    $(document).ready(function () {
        editor = new $.fn.dataTable.Editor({
            ajax: {
                url: "/api/links",
                data: {
                    "_csrf" : "<%= csrfToken %>"
                }
            },
            // template: "#customForm",
            fields: [
                {
                    label: "uuid",
                    name: "links.uuid",
                    type: "hidden"
                },
                {
                    label: "Name",
                    name: "links.name",
                    // fieldInfo: "The job role name.  This must be unique"
                },
                {
                    label: "Description",
                    name: "links.description",
                    // fieldInfo: 'The job role description'
                },
                {
                    label: "URL",
                    name: "links.url"
                },
                {
                    label: "Icon",
                    name: "links.icon",
                    type: "upload",
                    display: function (file_id) {
                        if (file_id !== 'null' && file_id) {
                            return '<img src="' + editor.file('icons', file_id).web_path + '"/>';
                        }
                    },
                    clearText: "Clear",
                    noImageText: 'No Image',
                    ajax: {
                        url: "/api/links",
                        data: {
                            "_csrf" : "<%= csrfToken %>"
                        }
                    },
                }
            ]
        });


        editor.on('submitSuccess', function (e, json, data, action) {
            if (action === 'create') {
                createCard(data);
            }
        });


        $('#addLink').on('click', function () {
            // editor.field('links.name').enable();
            // editor.set('job_roles.locked', 'false');
            editor
                .title('Create New Link')
                .buttons('Create', 'Cancel')
                .create();
        });

        $('#cards').on('click', 'button.edit', function () {
            // editor.field('links.name').disable();
            editor
                .title('Edit Link: ' + $(this).data('name'))
                .buttons('Save')
                .edit($(this).data('id'));
        });

        $('#cards').on('click', 'button.delete', function () {
            editor
                .title('Delete Link: ' + $(this).data('name'))
                .buttons('Delete')
                .message('Are you sure you want to delete this link?')
                .remove($(this).data('id'));
        });


        $.ajax({
            url: '/api/links',
            dataType: 'json',
            method: 'GET',
            success: function (json) {
                for (var i = 0, ien = json.data.length; i < ien; i++) {
                    createCard(json.data[i])
                }
            }
        });


    });


</script>

<%- include('./includes/end.ejs') %>

This question has an accepted answers - jump to answer

Answers

  • allanallan Posts: 61,446Questions: 1Answers: 10,054 Site admin

    Is the csrf middleware rejecting the upload? If you look in the browser's network inspector for the "header" data (i.e. what was sent to the server) on upload, does it include the csrf code given in the data property?

    If not, could you try:

    ajax: '/api/links',
    ajaxData: function (d) {
      d.append('_csrf', '<%= csrfToken %>');
    }
    

    Thanks,
    Allan

  • djagercddjagercd Posts: 13Questions: 4Answers: 0

    Allan,

    I have added that into the code and it still gives me the same error. The req data in the server is:

    req:
    { url: '/links',
    headers:
    { host: 'localhost:3000',
    connection: 'keep-alive',
    'content-length': '980',
    accept: 'application/json, text/javascript, /; q=0.01',
    origin: 'http://localhost:3000',
    'x-requested-with': 'XMLHttpRequest',
    'user-agent':
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
    dnt: '1',
    'content-type':
    'multipart/form-data; boundary=----WebKitFormBoundary2T8zsPHenzRl8QgM',
    referer: 'http://localhost:3000/links',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'en,af;q=0.9,en-US;q=0.8,la;q=0.7',
    cookie:
    'session_store_key=s%3AoX3kjZAb5AzOftRrjVCrcJ1BYGKPtXAt.tJuLYQDg3uU4p%2FzfnDIBGHr9qtJrfB60MkQIRxYnXW4' },
    method: 'POST',
    httpVersion: '1.1',
    originalUrl: '/links',
    query: {} } },
    timestamp: '2019-03-20T11:24:12.514Z',

  • djagercddjagercd Posts: 13Questions: 4Answers: 0

    I have managed to solve the issue by using CSRF's header abilities.

  • allanallan Posts: 61,446Questions: 1Answers: 10,054 Site admin

    As in using ajax: { headers: ... }?

    Thanks,
    Allan

  • djagercddjagercd Posts: 13Questions: 4Answers: 0
    edited March 2019

    Yes. That passes the correct information to csurf. I am now getting a different error when trying to upload the file. It is giving me a "Uncaught Unknown file table name: files" on the client side and the following on the serverside

    { level: 'error',
    message: "TypeError: Cannot read property 'toLowerCase' of undefined",
    timestamp: '2019-03-22T10:40:01.422Z',
    [Symbol(message)]:
    {"level":"error","message":"TypeError: Cannot read property 'toLowerCase' of undefined"} }
    error: TypeError: Cannot read property 'toLowerCase' of undefined

    I have copied the example from the Editor example downloads.

    Here is my code serverside code.

    //Serverside for Links-api
    'use strict';
    
    const
        path = require('path'),
        process = require('process'),
        fs = require('fs'),
        {
            Editor,
            Field,
            Validate,
            Format,
            Options,
            Upload,
            promisify,
        } = require('datatables.net-editor-server'),
        db = require('../db'),
        uuidv4 = require('uuid/v4'),
        {logger} = require('../logger');
    
    let unlink = promisify(fs.unlink);
    let icondir = path.join(path.dirname(process.mainModule.filename), 'public', 'uploads');
    let iconfile = path.join(icondir, '{id}.{extn}');
    
    exports.LinksAPI = (req, res, next) => {
    
        const editor = new Editor(db, 'links', 'links.uuid')
            .fields(
                new Field('links.uuid'),
                new Field('links.name')
                    .validator(Validate.required(
                        new Validate.Options(
                            {message: 'The link name is required.'}
                        ))),
                new Field('links.description')
                    .validator(Validate.required(new Validate.Options(
                        {message: 'The link description is required.'}
                    ))),
                new Field('links.url')
                    .validator(Validate.required(new Validate.Options(
                        {message: 'The link url is required.'}
                    )))
                    .validator(Validate.dbUnique(new Validate.Options(
                        {message: 'There is already a link with that URL.'}
                    )))
                    .validator(Validate.url(new Validate.Options(
                        {message: 'You have specified an invalid URL.'}
                    ))),
                new Field('links.icon')
                    .setFormatter(Format.ifEmpty(null))
                    .upload(
                        new Upload(iconfile)
                            .db('files', 'id', {
                                filename: Upload.Db.FileName,
                                filesize: Upload.Db.FileSize,
                                web_path: '/uploads/{id}.{extn}',
                                system_path: Upload.Db.SystemPath
                            })
    
                            .dbClean(async function (data) {
                                for (let i = 0, ien = data.length; i < ien; i++) {
                                    await unlink(data[i].system_path);
                                }
                                return true;
                            })
                    )
                    .validator(Validate.fileSize(500000, 'Files must be smaller than 500K'))
                    .validator(
                        Validate.fileExtensions(
                            ['png', 'jpeg', 'jpg', 'gif'],
                            'Only image files can be uploaded (png, jpg, jpeg and gif)'
                        )
                    )
                    .options(new Options()
                        .table('files')
                        .value('id')
                        .label('filename')
                    )
            )
            .leftJoin(
                'files',
                'files.id',
                '=',
                'links.icon'
            );
    
    
        editor.on('preCreate', (editor, values) => {
            editor.field('links.uuid').setValue(uuidv4());
        });
    
    
        editor.process(req.body, req.files)
            .then(function () {
                res.json(editor.data());
            })
            .catch(err => {
                logger.log('error', err.toString());
                res.send({});
            });
    };
    
    

    I have tried it with and without the leftjoin to the 'files' table.

    Any ideas of what I am doing wrong here?

    Here is my ejs as well.

    <%- include('./includes/head.ejs') %>
    
    </head>
    
    <body>
    <%- include('./includes/navigation.ejs') %>
    
    
    <main class="main">
        <div class="container-fluid">
            <div id="list" class="row">
                <div class="col-lg-4">
                    <button id="addLink" class="btn">Add Link</button>
                </div>
                <div class="col">
                    <input id="searchText" class="active" placeholder="enter search...">
                </div>
            </div>
            <div class="container-fluid">
                <div id="cards" class="row">
    
                </div>
            </div>
        </div>
    </main>
    <%- include('./includes/end-scripts.ejs') %>
    
    <script type="text/javascript">
        var editor;
    
        function createCard(data) {
            var id = data.DT_RowId;
            var html =
                '<div class="card col-sm-3" data-editor-id="' + id + '" id="' + id + '" data-name="' + data.links.name + '">' +
                '<div class="card-header">' +
                '<a class="hidden" data-editor-field="links.uuid">' + data.links.uuid + '</a>' +
                '<h5 class="card-title" data-editor-field="links.name">' + data.links.name + '</h5>' +
                '</div>' +
                '<div class="card-body">' +
                '<p class="card-text">' +
                '<p>Description:</p>' +
                '<p data-editor-field="links.description">' + data.links.description + '</p>' +
                '<p class="hidden" data-editor-field="links.icon">' + data.links.icon + '</p>' +
                '<a href="' + data.links.url + '" target="_blank"  data-editor-field="links.url">' + data.links.url + '</a>' +
                '</p>' +
                '</div>' +
                '<div class="card-footer text-center">' +
                '<button data-id="' + id + '" data-name="' + data.links.name + '" class="btn btn-sized edit">Edit</button>';
    
            //TODO
            //  Add code to only allow delete if user is admin
    
    
            //if (!data.menus.locked) {
    
            html = html + '<button data-id="' + id + '" data-name="' + data.links.name + '" class="btn btn-sized-danger delete">Delete</button>'
    
            // }
    
            html = html + '</div>' +
                '</div>';
    
            $(html).appendTo('#cards');
    
        }
    
        $(document).ready(function () {
    
    
            editor = new $.fn.dataTable.Editor({
                ajax: {
                    url: "/api/links",
                    headers: {
                        "CSRF-Token": "<%= csrfToken %>"
                    }
                },
                // template: "#customForm",
                fields: [
                    {
                        label: "uuid",
                        name: "links.uuid",
                        type: "hidden"
                    },
                    {
                        label: "Name",
                        name: "links.name",
                        // fieldInfo: "The job role name.  This must be unique"
                    },
                    {
                        label: "Description",
                        name: "links.description",
                        // fieldInfo: 'The job role description'
                    },
                    {
                        label: "URL",
                        name: "links.url"
                    },
                    {
                        label: "Icon",
                        name: "links.icon",
                        type: "upload",
                        display: function (file_id) {
                            return '<img src="' + editor.file('files', file_id).web_path + '"/>';
                        },
                        clearText: "Clear",
                        noImageText: 'No Image',
                        ajax: {
                            url: "/api/links",
                            headers: {
                                "CSRF-Token": "<%= csrfToken %>"
                            }
                        }
                    }
                ]
            });
    
    
            editor.on('submitSuccess', function (e, json, data, action) {
                if (action === 'create') {
                    createCard(data);
                }
            });
    
    
            $('#addLink').on('click', function () {
                // editor.field('links.name').enable();
                // editor.set('job_roles.locked', 'false');
                editor
                    .title('Create New Link')
                    .buttons('Create', 'Cancel')
                    .create();
            });
    
            $('#cards').on('click', 'button.edit', function () {
                // editor.field('links.name').disable();
                editor
                    .title('Edit Link: ' + $(this).data('name'))
                    .buttons('Save')
                    .edit($(this).data('id'));
            });
    
            $('#cards').on('click', 'button.delete', function () {
                editor
                    .title('Delete Link: ' + $(this).data('name'))
                    .buttons('Delete')
                    .message('Are you sure you want to delete this link?')
                    .remove($(this).data('id'));
            });
    
    
            $.ajax({
                url: '/api/links',
                dataType: 'json',
                method: 'GET',
                success: function (json) {
                    for (var i = 0, ien = json.data.length; i < ien; i++) {
                        createCard(json.data[i])
                    }
                }
            });
    
    
        });
    
    
    </script>
    
    <%- include('./includes/end.ejs') %>
    
    
    
  • allanallan Posts: 61,446Questions: 1Answers: 10,054 Site admin

    Can you show me the JSON return from the server when you upload a file please?

    Allan

  • djagercddjagercd Posts: 13Questions: 4Answers: 0
    edited March 2019

    Hi Allan

    If I try the code with a join to the files table (as above) I get this. The links.icon is populated as options with the files, but the files subtable is empty.

    If I remove the options and the join (as per the example) it returns a blank set as well for files.

    {data: [{DT_RowId: "row_73665ac2-6c47-4131-bd80-70cc047325b9",…}], fieldErrors: [], files: {files: {}},…}
    data: [{DT_RowId: "row_73665ac2-6c47-4131-bd80-70cc047325b9",…}]
    0: {DT_RowId: "row_73665ac2-6c47-4131-bd80-70cc047325b9",…}
    DT_RowId: "row_73665ac2-6c47-4131-bd80-70cc047325b9"
    links: {uuid: "73665ac2-6c47-4131-bd80-70cc047325b9", name: "Test", description: "Test",…}
    description: "Test"
    icon: 1
    name: "Test"
    url: "http://www.google.com"
    uuid: "73665ac2-6c47-4131-bd80-70cc047325b9"
    fieldErrors: []
    files: {files: {}}
    files: {}
    options: {links.icon: [{label: "SDM grey.jpg", value: 4}, {label: "SDM grey.jpg", value: 6},…]}
    links.icon: [{label: "SDM grey.jpg", value: 4}, {label: "SDM grey.jpg", value: 6},…]
    0: {label: "SDM grey.jpg", value: 4}
    1: {label: "SDM grey.jpg", value: 6}
    2: {label: "SDM red.png", value: 7}
    3: {label: "SDM.png", value: 3}
    4: {label: "SimpleData Management logo grey.pdf", value: 5}
    5: {label: "icon.png", value: 2}
    
  • djagercddjagercd Posts: 13Questions: 4Answers: 0

    Hi Allan,

    Once I added the DataTable to the page it all worked. So you cannot use file upload with Editor in standalone mode.

    Regards

  • allanallan Posts: 61,446Questions: 1Answers: 10,054 Site admin
    Answer ✓

    The file information is retrieved by DataTables' Ajax request when populating the data into the table (Editor listens for the xhr event to see when that happens). In standalone mode there is no Ajax request to piggyback that information onto, so in that mode, you'd need to populate the data for existing files by making an Ajax request to get that data and then assigning it to $.fn.dataTable.Editor.files[ table ].

    That should happen automatically when uploading a new file in standalone mode though - its just the initial population that would be a problem.

    Allan

This discussion has been closed.