richtext-procs.tcl

CKEditor 4 integration with the richtext widget of acs-templating. This script defines the following public procs: ::richtext-ckeditor4::initialize_widget ::richtext-ckeditor4::render_widgets ::richtext::ckeditor4::resource_info ::richtext::ckeditor4::add_editor

Location:
packages/richtext-ckeditor4/tcl/richtext-procs.tcl
Created:
1 Jan 2016
Author:
Gustaf Neumann
CVS Identification:
$Id: richtext-procs.tcl,v 1.14.2.33 2024/07/30 10:25:45 gustafn Exp $

Procedures in this file

Detailed information

richtext::ckeditor4::add_editor (public)

 richtext::ckeditor4::add_editor [ -ck_package ck_package ] \
    [ -version version ] [ -adapters adapters ] [ -order order ]

Add the necessary JavaScript and other files to the current page. The naming is modeled after "add_script", "add_css", ... but is intended to care about everything necessary, including the content security policies. Similar naming conventions should be used for other editors as well. This function can be as well used from other packages, such e.g. from the xowiki form-fields, which provide a much higher customization.

Switches:
-ck_package (optional)
-version (optional)
-adapters (optional)
-order (optional, defaults to "10")

Partial Call Graph (max 5 caller/called nodes):
%3 richtext::ckeditor4::initialize_widget richtext::ckeditor4::initialize_widget (public) richtext::ckeditor4::add_editor richtext::ckeditor4::add_editor richtext::ckeditor4::initialize_widget->richtext::ckeditor4::add_editor richtext::ckeditor4::resource_info richtext::ckeditor4::resource_info (public) richtext::ckeditor4::add_editor->richtext::ckeditor4::resource_info security::csp::require security::csp::require (public) richtext::ckeditor4::add_editor->security::csp::require template::head::add_javascript template::head::add_javascript (public) richtext::ckeditor4::add_editor->template::head::add_javascript

Testcases:
No testcase defined.

richtext::ckeditor4::download (private)

 richtext::ckeditor4::download [ -ck_package ck_package ] \
    [ -version version ]

Download the CKeditor package in the specified version and put it into a directory structure similar to the CDN structure to allow installation of multiple versions. When the local structure is available, it will be used by initialize_widget. Notice, that for this automated download, the "unzip" program must be installed and $::acs::rootdir/packages/www must be writable by the web server.

Switches:
-ck_package (optional)
-version (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 packages/richtext-ckeditor4/www/sitewide-admin/download.tcl packages/richtext-ckeditor4/ www/sitewide-admin/download.tcl richtext::ckeditor4::download richtext::ckeditor4::download packages/richtext-ckeditor4/www/sitewide-admin/download.tcl->richtext::ckeditor4::download packages/richtext-ckeditor5/www/sitewide-admin/download.tcl packages/richtext-ckeditor5/ www/sitewide-admin/download.tcl packages/richtext-ckeditor5/www/sitewide-admin/download.tcl->richtext::ckeditor4::download acs_package_root_dir acs_package_root_dir (public) richtext::ckeditor4::download->acs_package_root_dir ad_file ad_file (public) richtext::ckeditor4::download->ad_file richtext::ckeditor4::resource_info richtext::ckeditor4::resource_info (public) richtext::ckeditor4::download->richtext::ckeditor4::resource_info util::json2dict util::json2dict (public) richtext::ckeditor4::download->util::json2dict util::resources::download util::resources::download (public) richtext::ckeditor4::download->util::resources::download

Testcases:
No testcase defined.

richtext::ckeditor4::initialize_widget (public)

 richtext::ckeditor4::initialize_widget [ -form_id form_id ] \
    [ -text_id text_id ] [ -options options ]

Initialize an CKEditor 4 richtext editor widget.

Switches:
-form_id (optional)
-text_id (optional)
-options (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 apm_package_id_from_key apm_package_id_from_key (public) export_vars export_vars (public) lang::conn::language lang::conn::language (public) parameter::get parameter::get (public) richtext::ckeditor4::add_editor richtext::ckeditor4::add_editor (public) richtext::ckeditor4::initialize_widget richtext::ckeditor4::initialize_widget richtext::ckeditor4::initialize_widget->apm_package_id_from_key richtext::ckeditor4::initialize_widget->export_vars richtext::ckeditor4::initialize_widget->lang::conn::language richtext::ckeditor4::initialize_widget->parameter::get richtext::ckeditor4::initialize_widget->richtext::ckeditor4::add_editor

Testcases:
No testcase defined.

richtext::ckeditor4::render_widgets (public)

 richtext::ckeditor4::render_widgets

Render the ckeditor4 rich-text widgets. This function is created at a time when all rich-text widgets of this page are already initialized. The function is controlled via the global variables ::acs_blank_master(ckeditor4) ::acs_blank_master__htmlareas

Partial Call Graph (max 5 caller/called nodes):
%3

Testcases:
No testcase defined.

richtext::ckeditor4::resource_info (public)

 richtext::ckeditor4::resource_info [ -ck_package ck_package ] \
    [ -version version ]

Get information about available version(s) of CKEditor, either from the local filesystem, or from CDN.

Switches:
-ck_package (optional)
-version (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 packages/richtext-ckeditor4/www/sitewide-admin/index.tcl packages/richtext-ckeditor4/ www/sitewide-admin/index.tcl richtext::ckeditor4::resource_info richtext::ckeditor4::resource_info packages/richtext-ckeditor4/www/sitewide-admin/index.tcl->richtext::ckeditor4::resource_info richtext::ckeditor4::add_editor richtext::ckeditor4::add_editor (public) richtext::ckeditor4::add_editor->richtext::ckeditor4::resource_info richtext::ckeditor4::download richtext::ckeditor4::download (private) richtext::ckeditor4::download->richtext::ckeditor4::resource_info acs_package_root_dir acs_package_root_dir (public) richtext::ckeditor4::resource_info->acs_package_root_dir parameter::get_global_value parameter::get_global_value (public) richtext::ckeditor4::resource_info->parameter::get_global_value

Testcases:
No testcase defined.
[ hide source ] | [ make this the default ]

Content File Source

ad_library {

    CKEditor 4 integration with the richtext widget of acs-templating.

    This script defines the following public procs:

    ::richtext-ckeditor4::initialize_widget
    ::richtext-ckeditor4::render_widgets
    ::richtext::ckeditor4::resource_info
    ::richtext::ckeditor4::add_editor


    @author Gustaf Neumann
    @creation-date 1 Jan 2016
    @cvs-id $Id: richtext-procs.tcl,v 1.14.2.33 2024/07/30 10:25:45 gustafn Exp $
}

namespace eval ::richtext::ckeditor4 {
    variable parameter_info
    
    #
    # The CKeditor 4 configuration can be tailored via the NaviServer
    # config file:
    #
    # ns_section ns/server/${server}/acs/richtext-ckeditor4
    #        ns_param CKEditorVersion   4.22.1
    #        ns_param CKEditorPackage   full
    #        ns_param CKFinderURL       /acs-content-repository/ckfinder
    #        ns_param StandardPlugins   uploadimage
    #

    set parameter_info {
        package_key richtext-ckeditor4
        parameter_name CKEditorVersion
        default_value 4.22.1
    }
    
    set package_id [apm_package_id_from_key "richtext-ckeditor4"]
    set ::richtext::ckeditor4::ckfinder_url [parameter::get \
                                                 -package_id $package_id \
                                                 -parameter CKFinderURL \
                                                 -default /acs-content-repository/ckfinder]
    set ::richtext::ckeditor4::standard_plugins [parameter::get \
                                                     -package_id $package_id \
                                                     -parameter StandardPlugins \
                                                     -default ""]

    #
    # The "ck_package" might be "basic", "standard", of "full";
    #
    # Use "custom" for customized downloads, expand the downloaded zip file in
    #    richtext-ckeditor4/www/resources/$version
    # and rename the expanded top-folder from "ckeditor" to "custom"
    #
    set ::richtext::ckeditor4::ck_package [parameter::get \
                                               -package_id $package_id \
                                               -parameter CKEditorPackage \
                                               -default "standard"]

    d_proc initialize_widget {
        -form_id
        -text_id
        {-options {}}
    } {

        Initialize an CKEditor 4 richtext editor widget.

    } {
        ns_log debug "CKEditor 4: initialize instance with <$options>"

        # Allow per default all CSS-classes, unless the user has specified
        # it differently
        if {![dict exists $options extraAllowedContent]} {
            dict set options extraAllowedContent {*(*)}
        }

        #
        # The richtext widget might be specified by "options {editor
        # ckeditor4}" or via the package parameter "RichTextEditor" of
        # acs-templating.
        #
        # The following options handled by the CKEditor integration
        # can be specified in the widget spec of the richtext widget:
        #
        #      plugins skin customConfig spellcheck
        #
        set ckOptionsList {}

        if {![dict exists $options spellcheck]} {
            set package_id [apm_package_id_from_key "richtext-ckeditor4"]
            dict set options spellcheck [parameter::get \
                                             -package_id $package_id \
                                             -parameter "SCAYT" \
                                             -default "false"]
        }
        # For the native spellchecker, one has to hold "ctrl" or "cmd"
        # with the right click.

        lappend ckOptionsList \
            "language: '[lang::conn::language]'" \
            "disableNativeSpellChecker: false" \
            "versionCheck: false" \
            "scayt_autoStartup: [dict get $options spellcheck]"

        #
        # Get the property "displayed_object_id" from the call-stack
        #
        for {set l 0} {$l < [info level]} {incr l} {
            set propVar __adp_properties(displayed_object_id)
            if {[uplevel #$l [list info exists $propVar]]} {
                set displayed_object_id [uplevel #$l [list set $propVar]]
                break
            }
        }

        #ns_log notice "ckeditor initialize_widget: displayed_object_id [info exists displayed_object_id]"
        if {[info exists displayed_object_id]} {
            #
            # If we have a displayed_object_id, configure it for the
            # plugins "filebrowser" and "uploadimage".
            #
            set image_upload_url [export_vars \
                                      -base $::richtext::ckeditor4::ckfinder_url/uploadimage {
                                          {object_id $displayed_object_id} {type Images}
                                      }]
            set file_upload_url [export_vars \
                                     -base $::richtext::ckeditor4::ckfinder_url/upload {
                                         {object_id $displayed_object_id} {type Files} {command QuickUpload}
                                     }]
            set file_browse_url [export_vars \
                                     -base $::richtext::ckeditor4::ckfinder_url/browse {
                                         {object_id $displayed_object_id} {type Files}
                                     }]
            set image_browse_url [export_vars \
                                      -base $::richtext::ckeditor4::ckfinder_url/browse {
                                          {object_id $displayed_object_id} {type Images}
                                      }]
            lappend ckOptionsList \
                "filebrowserImageUploadUrl: '$image_upload_url'" \
                "filebrowserImageBrowseUrl: '$image_browse_url'" \
                "filebrowserBrowseUrl: '$file_browse_url'" \
                "filebrowserUploadUrl: '$file_upload_url'" \
                "filebrowserWindowWidth: '800'" \
                "filebrowserWindowHeight: '600'"
        }

        set plugins [split $::richtext::ckeditor4::standard_plugins ,]
        if {[dict exists $options plugins]} {
            lappend plugins {*}[split [dict get $options plugins] ,]
        }
        if {[llength $plugins] > 0} {
            lappend ckOptionsList "extraPlugins: '[join $plugins ,]'"
        }
        if {[dict exists $options skin]} {
            lappend ckOptionsList "skin: '[dict get $options skin]'"
        }
        if {[dict exists $options customConfig]} {
            lappend ckOptionsList \
                "customConfig: '[dict get $options customConfig]'"
        }
        if {[dict exists $options extraAllowedContent]} {
            lappend ckOptionsList \
                "extraAllowedContent: '[dict get $options extraAllowedContent]'"
        }

        set ckOptions [join $ckOptionsList ", "]
        ns_log debug "CKEditor 4: final ckOptions = $ckOptions"

        #
        # Add the configuration via body script
        #
        template::add_script -section body -script [subst {
            CKEDITOR.replace( '$text_id', {$ckOptions} );
        }]

        #
        # Load the editor and everything necessary to the current page.
        #
        ::richtext::ckeditor4::add_editor

        #
        # do we need render_widgets?
        #
        return ""
    }


    ad_proc render_widgets {} {

        Render the ckeditor4 rich-text widgets. This function is created
        at a time when all rich-text widgets of this page are already
        initialized. The function is controlled via the global variables

        ::acs_blank_master(ckeditor4)
        ::acs_blank_master__htmlareas

    } {
        #
        # In case no ckeditor4 instances are created, nothing has to be
        # done.
        #
        if {![info exists ::acs_blank_master(ckeditor4)]} {
            return
        }
        #
        # Since "template::head::add_javascript -src ..." prevents
        # loading the same resource multiple times, we can perform the
        # load in the per-widget initialization and we are done here.
        #
    }

    d_proc ::richtext::ckeditor4::resource_info {
        {-ck_package ""}
        {-version ""}
    } {

        Get information about available version(s) of CKEditor, either
        from the local filesystem, or from CDN.

    } {
        variable parameter_info
        #
        # If no version or CKeditor package are specified, use the
        # namespaced variables as default.
        #
        if {$version eq ""} {
            dict with parameter_info {
                set version [::parameter::get_global_value \
                                 -package_key $package_key \
                                 -parameter $parameter_name \
                                 -default $default_value]
            }
        }
        
        if {$ck_package eq ""} {
            set ck_package ${::richtext::ckeditor4::ck_package}
        }

        #
        # Setup variables for access via CDN vs. local resources.
        #
        set resourceDir [acs_package_root_dir richtext-ckeditor4/www/resources]
        set cdn         //cdn.ckeditor.com

        set suffix $version/$ck_package/ckeditor.js
        if {[file exists $resourceDir/$version/$ck_package]} {
            set prefix  /resources/richtext-ckeditor4/$version
            set cdnHost ""
        } else {
            set prefix $cdn/$version
            set cdnHost cdn.ckeditor.com
        }

        #
        # Return the dict with at least the required fields
        #
        lappend result \
            resourceName "CKEditor 4" \
            resourceDir $resourceDir \
            cdn $cdn \
            cdnHost $cdnHost \
            prefix $prefix \
            cssFiles {} \
            jsFiles  {} \
            extraFiles {} \
            downloadURLs http://download.cksource.com/CKEditor/CKEditor/CKEditor%20${version}/ckeditor_${version}_${ck_package}.zip \
            urnMap {} \
            plugins {
                a11yhelp about clipboard dialog image link magicline pastefromgdocs pastefromlibreoffice
                pastefromword pastetools scayt specialchar table tableselection tabletools widget
            } \
            versionCheckAPI {cdn cdnjs library ckeditor count 20} \
            vulnerabilityCheck {service snyk library ckeditor4} \
            parameterInfo $parameter_info \
            configuredVersion $version \

        return $result
    }

    d_proc ::richtext::ckeditor4::add_editor {
        {-ck_package ""}
        {-version ""}
        {-adapters ""}
        {-order 10}
    } {

        Add the necessary JavaScript and other files to the current
        page. The naming is modeled after "add_script""add_css",
        ... but is intended to care about everything necessary,
        including the content security policies. Similar naming
        conventions should be used for other editors as well.

        This function can be as well used from other packages, such
        e.g. from the xowiki form-fields, which provide a much higher
        customization.

    } {
        if {$ck_package eq ""} {
            set ck_package ${::richtext::ckeditor4::ck_package}
        }        
        #ns_log notice "richtext::ckeditor4::add_editor -version $version -ck_package $ck_package"
        
        set resource_info [::richtext::ckeditor4::resource_info \
                               -ck_package $ck_package \
                               -version $version]
        set version [dict get $resource_info configuredVersion]
        set prefix [dict get $resource_info prefix]
        #ns_log notice "richtext::ckeditor4::add_editor loading from $prefix"

        if {[dict exists $resource_info cdnHost] && [dict get $resource_info cdnHost] ne ""} {
            security::csp::require script-src [dict get $resource_info cdnHost]
            security::csp::require style-src  [dict get $resource_info cdnHost]
            security::csp::require img-src    [dict get $resource_info cdnHost]
        }
        #ns_log notice "richtext::ckeditor4::add_editor SRC -src $prefix/$ck_package/ckeditor.js"
        template::head::add_javascript -order $order \
            -src $prefix/$ck_package/ckeditor.js

        foreach adapter $adapters {
            template::head::add_javascript -order $order.1 \
                -src $prefix/$ck_package/adapters/$adapter
        }

        #
        # Add required general directives for content security policies.
        #
        security::csp::require script-src 'unsafe-eval'
        security::csp::require -force script-src 'unsafe-inline'

        # this is needed currently for "imageUploadUrl"
        security::csp::require img-src data:
    }

    d_proc -private ::richtext::ckeditor4::download {
        {-ck_package ""}
        {-version ""}
    } {

        Download the CKeditor package in the specified version and put
        it into a directory structure similar to the CDN structure to
        allow installation of multiple versions. When the local
        structure is available, it will be used by initialize_widget.

        Notice, that for this automated download, the "unzip" program
        must be installed and $::acs::rootdir/packages/www must be
        writable by the web server.

    } {
        #
        # If no ck_package is specified, use the namespaced variable
        # as default.
        #
        if {$ck_package eq ""} {
            set ck_package ${::richtext::ckeditor4::ck_package}
        }

        set resource_info [::richtext::ckeditor4::resource_info \
                               -ck_package $ck_package \
                               -version $version]
        set version [dict get $resource_info configuredVersion]

        set downloadFromCDNnjs 0
        if {$downloadFromCDNnjs} {
            #
            # If you really want to use this, you should also clear
            # "downloadURLs" in resource_info to avoid the version
            # check on the tar file in
            # :util::resources::is_installed_locally. For this
            # piecewise download the tar file does not exist.
            #
            set install_dir_name [acs_package_root_dir richtext-ckeditor4]/www/resources/$version/standard
            set download_prefix https://cdnjs.cloudflare.com/ajax/libs/ckeditor/$version/

            file mkdir $install_dir_name
            set r [ns_http run https://api.cdnjs.com/libraries/ckeditor/$version]
            set d [::util::json2dict [dict get $r body]]
            foreach fn [dict get $d files] {
                if {[string match *.min.* $fn]} continue
                if {[regexp {plugins/([^/]+)/} $fn . pluginName]} {
                    if {$pluginName ni [dict get $resource_info plugins]} {
                        continue
                    }
                }
                set result [::util::resources::download_helper -url $download_prefix/$fn]
                #ns_log notice "... returned status code [dict get $result status]"
                set spool_fn [dict get $result file]

                set subdir [ad_file dirname $install_dir_name/$fn]
                if {![ad_file isdirectory $subdir]} {
                    file mkdir $subdir
                }
                #ns_log notice "mv $spool_fn $install_dir_name/$fn"
                file rename -force -- $spool_fn $install_dir_name/$fn
            }
            return
        }

        ::util::resources::download -resource_info $resource_info

        set resourceDir [dict get $resource_info resourceDir]

        #
        # Do we have unzip installed?
        #
        set unzip [::util::which unzip]
        if {$unzip eq ""} {
            error "can't install CKeditor locally; no unzip program found on PATH"
        }

        #
        # Do we have a writable output directory under resourceDir?
        #
        if {![file isdirectory $resourceDir/$version]} {
            file mkdir $resourceDir/$version
        }
        if {![file writable $resourceDir/$version]} {
            error "directory $resourceDir/$version is not writable"
        }

        #
        # So far, everything is fine, unpack the editor package.
        #
        foreach url [dict get $resource_info downloadURLs] {
            set fn [file tail $url]
            util::unzip -overwrite -source $resourceDir/$version/$fn -destination $resourceDir/$version
            file rename -- \
                $resourceDir/$version/ckeditor \
                $resourceDir/$version/$ck_package
        }
    }
}


# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: