• Publicity: Public Only All

xowiki-procs.tcl

XoWiki - main library classes and objects

Location:
packages/xowiki/tcl/xowiki-procs.tcl
Created:
2006-01-10
Author:
Gustaf Neumann
CVS Identification:
$Id: xowiki-procs.tcl,v 1.548 2024/10/24 17:02:23 gustafn Exp $

Procedures in this file

Detailed information

xowiki::update_item_index (public)

 xowiki::update_item_index -item_id item_id [ -name name ] \
    [ -package_id package_id ] [ -parent_id parent_id ] \
    [ -publish_status publish_status ] \
    [ -page_template page_template ] [ -assignee assignee ] \
    [ -state state ] [ -hstore_attributes hstore_attributes ]

Helper function to update single or multiple fields of the xowiki_form_instance_item_index. Call this function only when updating fields of the xowiki_form_instance_item_index in cases where the standard API based on save and save_use can not be used.

Switches:
-item_id (required)
-name (optional)
-package_id (optional)
-parent_id (optional)
-publish_status (optional)
-page_template (optional)
-assignee (optional)
-state (optional)
-hstore_attributes (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 test_slot_interactions slot_interactions (test xowiki) xowiki::update_item_index xowiki::update_item_index test_slot_interactions->xowiki::update_item_index xowiki::hstore::dict_as_hkey xowiki::hstore::dict_as_hkey (public) xowiki::update_item_index->xowiki::hstore::dict_as_hkey xowiki::FormPage instproc set_live_revision xowiki::FormPage instproc set_live_revision (public) xowiki::FormPage instproc set_live_revision->xowiki::update_item_index xowiki::FormPage instproc update_attribute_from_slot xowiki::FormPage instproc update_attribute_from_slot (public) xowiki::FormPage instproc update_attribute_from_slot->xowiki::update_item_index xowiki::Package instproc reparent xowiki::Package instproc reparent (public) xowiki::Package instproc reparent->xowiki::update_item_index

Testcases:
slot_interactions
[ hide source ] | [ make this the default ]

Content File Source

::xo::library doc {
  XoWiki - main library classes and objects

  @creation-date 2006-01-10
  @author Gustaf Neumann
  @cvs-id $Id: xowiki-procs.tcl,v 1.548 2024/10/24 17:02:23 gustafn Exp $
}

namespace eval ::xowiki {
  #
  # Create classes for different kind of pages
  #
  ::xo::db::CrClass create Page -superclass ::xo::db::CrItem \
      -pretty_name "#xowiki.Page_pretty_name#" -pretty_plural "#xowiki.Page_pretty_plural#" \
      -table_name "xowiki_page" -id_column "page_id" \
      -mime_type text/html \
      -slots {
        ::xo::db::CrAttribute create page_order \
            -sqltype ltree -validator page_order -default ""
        ::xo::db::CrAttribute create creator
        # The following slots are defined elsewhere, but we override
        # some default values, such as pretty_names, required state,
        # help text etc.
        ::xo::Attribute create name \
            -help_text #xowiki.Page-name-help_text# \
            -validator name \
            -spec "maxlength=400,required" \
            -required false ;#true
        #::xo::Attribute create title \
        #    -required false ;#true
        #::xo::Attribute create description \
        #    -spec "textarea,cols=80,rows=2"
        #::xo::Attribute create text \
        #    -spec "richtext"
        ::xo::Attribute create nls_language \
            -spec {select,options=[xowiki::locales]} \
            -default {[ad_conn locale]}
        #::xo::Attribute create publish_date \
        #    -spec date
        ::xo::Attribute create last_modified \
            -spec date
        ::xo::Attribute create creation_user \
            -spec user_id
      } \
      -extend_slot {title -required false} \
      -extend_slot {description -spec "textarea,cols=80,rows=2,label=#xowiki.Page-description#"} \
      -extend_slot {text -spec "richtext"} \
      -extend_slot {publish_date -spec "date,label=#xowiki.Page-publish_date#"} \
      -parameter {
        {render_adp 1}
        {do_substitutions 1}
        {absolute_links 0}
      } \
      -form ::xowiki::WikiForm

  if {$::xotcl::version < 1.5} {
    ad_log error "XOTcl 1.5 or newer is required. You seem to use XOTcl $::xotcl::version!"
  }

  ::xo::db::CrClass create PlainPage -superclass Page \
      -pretty_name "#xowiki.PlainPage_pretty_name#" -pretty_plural "#xowiki.PlainPage_pretty_plural#" \
      -table_name "xowiki_plain_page" -id_column "ppage_id" \
      -mime_type text/plain \
      -form ::xowiki::PlainWikiForm

  ::xo::db::CrClass create File -superclass Page \
      -pretty_name "#xowiki.File_pretty_name#" -pretty_plural "#xowiki.File_pretty_plural#" \
      -table_name "xowiki_file" -id_column "file_id" \
      -non_cached_instance_var_patterns {import_file do_substitutions} \
      -storage_type file \
      -form ::xowiki::FileForm \
      -parameter {
        {storage_type file}
      }

  ::xo::db::CrClass create PodcastItem -superclass File \
      -pretty_name "#xowiki.PodcastItem_pretty_name#" -pretty_plural "#xowiki.PodcastItem_pretty_plural#" \
      -table_name "xowiki_podcast_item" -id_column "podcast_item_id" \
      -slots {
        ::xo::db::CrAttribute create pub_date \
            -datatype date \
            -sqltype timestamp \
            -spec "date,format=YYYY_MM_DD_HH24_MI"
        ::xo::db::CrAttribute create duration \
            -help_text "#xowiki.PodcastItem-duration-help_text#"
        ::xo::db::CrAttribute create subtitle
        ::xo::db::CrAttribute create keywords \
            -help_text "#xowiki.PodcastItem-keywords-help_text#"
      } \
      -storage_type file \
      -form ::xowiki::PodcastForm


  ::xo::db::CrClass create PageTemplate -superclass Page \
      -pretty_name "#xowiki.PageTemplate_pretty_name#" -pretty_plural "#xowiki.PageTemplate_pretty_plural#" \
      -table_name "xowiki_page_template" -id_column "page_template_id" \
      -slots {
        ::xo::db::CrAttribute create anon_instances \
            -datatype boolean \
            -sqltype boolean -default "f"
      } \
      -form ::xowiki::PageTemplateForm

  ::xo::db::CrClass create PageInstance -superclass Page \
      -pretty_name "#xowiki.PageInstance_pretty_name#" -pretty_plural "#xowiki.PageInstance_pretty_plural#" \
      -table_name "xowiki_page_instance"  -id_column "page_instance_id" \
      -slots {
        ::xo::db::CrAttribute create page_template \
            -datatype integer \
            -references "cr_items(item_id) DEFERRABLE INITIALLY DEFERRED"
        ::xo::db::CrAttribute create instance_attributes \
            -sqltype long_text \
            -default ""
        #
        # To enable hstore for xowiki/xowf, for pre-existing
        # xowiki/xowf instancs, make sure, hstore is installed in your
        # PostgreSQL installation, e.g. via
        #
        #      $PGBIN/psql -U nsadmin -d oacs-head -tAc "create extension hstore"
        #
        # Then the condition [::xo::dc has_hstore] will be true and
        # hstore can be used in principle. During startup, xowiki will
        # create indices for the hkey column on
        #
        #   - xowiki_page_instance(hkey) and
        #   - xowiki_form_instance_item_index(hkey).
        #
        # The OpenACS content repository requires updates for its
        # automatically created views und update procs. This can be done via
        #
        #    xo::db::sql::content_type refresh_view -content_type ::xowiki::PageInstance
        #    xo::db::sql::content_type refresh_view -content_type ::xowiki::FormPage
        #
        # In order to use it for certain xowf/xowiki instances, set
        # the package parameter "use_hstore" of the xowiki/xowf
        # instances that want to use hstore to 1.
        #
        # To update all hkey attributes in e.g. a package mounted at /xowf
        # use:
        #
        #     ::xo::Package initialize -url /xowiki
        #     ::xowiki::hstore::update_hstore $package_id
        #     ::xowiki::hstore::update_form_instance_item_index -package_id $package_id
        #
        # The first update command updates xowiki_page_instance, the second
        # xowiki_form_instance_item_index.
        #
        if {[::xo::dc has_hstore]} {
          if {[xo::dc list check_type {select 1 from acs_datatypes where datatype = 'hstore'}] eq {}} {
            xo::dc dml add_datatype_hstore {
              insert into acs_datatypes (datatype, max_n_values, database_type, column_size)
              values  ('hstore', null, 'hstore', null)
            }
          }
          ::xo::db::CrAttribute create hkey \
              -sqltype hstore \
              -datatype hstore \
              -default ""
        }
      } \
      -form ::xowiki::PageInstanceForm \
      -edit_form ::xowiki::PageInstanceEditForm

  if {[::xo::dc has_hstore]} {
    ::xo::db::require index -table xowiki_page_instance -col hkey -using gist
  }

  ::xo::db::CrClass create Object -superclass PlainPage \
      -pretty_name "#xowiki.Object_pretty_name#" -pretty_plural "#xowiki.Object_pretty_plural#" \
      -table_name "xowiki_object"  -id_column "xowiki_object_id" \
      -mime_type text/plain \
      -form ::xowiki::ObjectForm

  ::xo::db::CrClass create Form -superclass PageTemplate \
      -pretty_name "#xowiki.Form_pretty_name#" -pretty_plural "#xowiki.Form_pretty_plural#" \
      -table_name "xowiki_form"  -id_column "xowiki_form_id" \
      -slots {
        ::xo::db::CrAttribute create form \
            -sqltype long_text \
            -default ""
        ::xo::db::CrAttribute create form_constraints \
            -sqltype long_text \
            -default "" \
            -validator form_constraints \
            -spec "textarea,cols=100,rows=5"
      } \
      -form ::xowiki::FormForm

  ::xo::db::CrClass create FormPage -superclass PageInstance \
      -pretty_name "#xowiki.FormPage_pretty_name#" -pretty_plural "#xowiki.FormPage_pretty_plural#" \
      -table_name "xowiki_form_page" -id_column "xowiki_form_page_id" \
      -non_cached_instance_var_patterns {__*} \
      -slots {
        ::xo::db::CrAttribute create assignee \
            -datatype integer \
            -references parties(party_id) \
            -spec "hidden"
        ::xo::db::CrAttribute create state -default ""
      }

  #
  # Create various extra tables, indices and views
  #
  ::xo::db::require index -table xowiki_form_page -col assignee
  ::xo::db::require index -table xowiki_page_instance -col page_template

  ::xo::db::require table xowiki_references [subst {
    reference   {integer references cr_items(item_id) on delete cascade}
    link_type   {[::xo::dc map_datatype text]}
    page        {integer references cr_items(item_id) on delete cascade}
  }]
  ::xo::db::require index -table xowiki_references -col reference
  ::xo::db::require index -table xowiki_references -col page

  ::xo::db::require table xowiki_unresolved_references [subst {
    page        {integer references cr_items(item_id) on delete cascade}
    parent_id   {integer references cr_items(item_id) on delete cascade}
    name        {[::xo::dc map_datatype text]}
    link_type   {[::xo::dc map_datatype text]}
  }]
  ::xo::db::require index -table xowiki_unresolved_references -col page
  ::xo::db::require index -table xowiki_unresolved_references -col parent_id
  ::xo::db::require index -table xowiki_unresolved_references -col name,parent_id

  ::xo::db::require table xowiki_last_visited {
    page_id    {integer references cr_items(item_id) on delete cascade}
    package_id integer
    user_id    integer
    count      integer
    time       timestamp
  }

  ::xo::db::require index -table xowiki_last_visited -col user_id,page_id -unique true
  ::xo::db::require index -table xowiki_last_visited -col user_id,package_id
  ::xo::db::require index -table xowiki_last_visited -col time
  ::xo::db::require index -table xowiki_last_visited -col page_id


  # Oracle has a limit of 3118 characters for keys, therefore, we
  # cannot use "text" as type for "tag"
  ::xo::db::require table xowiki_tags {
    item_id    {integer references cr_items(item_id) on delete cascade}
    package_id integer
    user_id    {integer references users(user_id)}
    tag        varchar(3000)
    time       timestamp
  }
  ::xo::db::require index -table xowiki_tags -col user_id,item_id
  ::xo::db::require index -table xowiki_tags -col tag,package_id
  ::xo::db::require index -table xowiki_tags -col user_id,package_id
  ::xo::db::require index -table xowiki_tags -col package_id
  ::xo::db::require index -table xowiki_tags -col user_id
  ::xo::db::require index -table xowiki_tags -col item_id

  ::xo::db::require index -table xowiki_page -col page_order \
      -using [expr {[::xo::dc has_ltree] ? "gist" : ""}]

  #
  # view: xowiki_page_live_revision
  #

  set sortkeys [expr {[db_driverkey ""] eq "oracle" ? "" : ", ci.tree_sortkey, ci.max_child_sortkey"}]
  ::xo::db::require view xowiki_page_live_revision \
      "select p.*, cr.*,ci.parent_id, ci.name, ci.locale, ci.live_revision, \
      ci.latest_revision, ci.publish_status, ci.content_type, ci.storage_type, \
      ci.storage_area_key $sortkeys \
          from xowiki_page p, cr_items ci, cr_revisions cr  \
          where p.page_id = ci.live_revision \
            and p.page_id = cr.revision_id  \
            and ci.publish_status <> 'production'"

  #
  # xowiki_form_instance_item_index:
  #
  #   A materialized table of xowiki formpage instances, containing
  #   just the item information, but combined with other attributes
  #   frequently used for indexing (like page_id, paren_id, ... hkey).
  #
  #   Rationale: The quality of indices on cr_revisions tend to
  #   decrease when there are many revisions stored in the database,
  #   since the number of duplicates increases due to non-live
  #   revisions. This table can be used for indexing just the live
  #   revision on the item level. Example query combining package_id
  #   and page_template:
  #
  #      select count(*) from xowiki_form_instance_item_index
  #      where package_id = 18255683
  #         and page_template = 20260757
  #         and publish_status='ready';
  #
  #   In order to get rid of the helper table
  #   "xowiki_form_instance_item_index", one may drop it, such it will
  #   be regenerated on the next load, but this make some time
  #   depending on the size of the instance:
  #
  #       drop table xowiki_form_instance_item_index cascade;
  #
  #
  #ns_logctl severity Debug(sql) on

  #
  # Create and populate xowiki_form_instance_item_index, when it does
  # not exist.
  #

  set populate {
      insert into xowiki_form_instance_item_index (
           item_id, name, package_id, parent_id, publish_status,
           page_template, assignee, state )
      select ci.item_id, ci.name, o.package_id, ci.parent_id, ci.publish_status,
           xpi.page_template, xfp.assignee, xfp.state
      from cr_items ci
      join xowiki_page_instance xpi on (ci.live_revision = xpi.page_instance_id)
      join xowiki_form_page xfp on (ci.live_revision = xfp.xowiki_form_page_id)
      join acs_objects o on (o.object_id = ci.item_id)
  }

  if {[::xo::dc has_hstore]} {
    #
    # Create table "xowiki_form_instance_item_index" with hstore column
    #
    # If the table does not exist, we have first to populate it, and
    # at the time, when all libraries are loaded, we have to update
    # the hkeys.
    nsv_set xowiki must_update_hkeys \
        [expr {[::xo::db::require exists_table xowiki_form_instance_item_index] == 0}]

    ::xo::db::require table xowiki_form_instance_item_index [subst {
      item_id             {integer references cr_items(item_id) on delete cascade}
      name                {character varying(400)}
      package_id          {integer}
      parent_id           {integer references cr_items(item_id) on delete cascade}
      publish_status      {character varying(40)}
      page_template       {integer references cr_items(item_id) on delete cascade}
      hkey                {hstore}
      assignee            {integer references parties(party_id) on delete cascade}
      state               {[::xo::dc map_datatype text]}
    }] $populate

    ::xo::db::require index -table xowiki_form_instance_item_index -col hkey -using gist

    set hkey_in_view "xi.hkey,"
  } else {
    #
    # Create table "xowiki_form_instance_item_index" without hstore column
    #
    ::xo::db::require table xowiki_form_instance_item_index [subst {
      item_id             {integer references cr_items(item_id) on delete cascade}
      name                {character varying(400)}
      package_id          {integer}
      parent_id           {integer references cr_items(item_id) on delete cascade}
      publish_status      {character varying(40)}
      page_template       {integer references cr_items(item_id) on delete cascade}
      assignee            {integer references parties(party_id) on delete cascade}
      state               {[::xo::dc map_datatype text]}
    }] $populate

    set hkey_in_view ""
  }

  ::xo::db::require index -table xowiki_form_instance_item_index -col item_id -unique true
  ::xo::db::require index -table xowiki_form_instance_item_index -col parent_id,name -unique true
  ::xo::db::require index -table xowiki_form_instance_item_index -col page_template
  ::xo::db::require index -table xowiki_form_instance_item_index -col page_template,package_id
  ::xo::db::require index -table xowiki_form_instance_item_index -col parent_id
  ::xo::db::require index -table xowiki_form_instance_item_index -col parent_id,page_template
  ::xo::db::require index -table xowiki_form_instance_item_index -col assignee

  #
  # Define helper views in connection with the form_instance_item_index:
  #
  # - xowiki_form_instance_item_view
  # - xowiki_form_instance_children
  # - xowiki_form_instance_attributes

  #
  # - xowiki_form_instance_item_view:
  #
  #   A view similar to xowiki_form_pagei, but containing already
  #   often extra-joined attributes like parent_id. This view returns
  #   only the values of the live revisions, and uses the
  #   form_instance_item_index for quick lookup. Example query to
  #   obtain all attributes necessary for object creation based on
  #   package_id and page_template.
  #
  #      select * from xowiki_form_instance_item_view
  #      where package_id = 18255683
  #         and page_template = 20260757
  #         and publish_status='ready';
  #
  if {[db_type] eq "postgresql"} {
    # More performant version using lateral. Oracle could support this
    # as well since version 12c.
    set sql [subst {
      SELECT
         xi.package_id, xi.parent_id, xi.name,
         $hkey_in_view xi.publish_status, xi.assignee, xi.state, xi.page_template, xi.item_id,
         o.object_id, o.object_type, o.title AS object_title,
         io.context_id,
         io.creation_date,
         io.creation_user,
         io.creation_ip,
         ci.storage_type,
         o.security_inherit_p,
         o.last_modified, o.modifying_user, o.modifying_ip,
         cr.revision_id, cr.title, content_revision__get_content(ci.live_revision) AS text,
         cr.description, cr.publish_date, cr.mime_type, cr.nls_language,
         (select xowiki_form_page_id from xowiki_form_page
          where xowiki_form_page_id = ci.live_revision) as xowiki_form_page_id,
         xowiki_page_instance.page_instance_id,
         xowiki_page_instance.instance_attributes,
         xowiki_page.page_id,
         xowiki_page.page_order,
         xowiki_page.creator
      FROM xowiki_form_instance_item_index xi
         inner join acs_objects io on object_id = xi.item_id,
         lateral (select live_revision, storage_type from cr_items where item_id = xi.item_id) ci
         left join cr_revisions cr on (cr.revision_id = ci.live_revision)
         left join acs_objects o on (o.object_id = ci.live_revision)
         left join xowiki_page on (xowiki_page.page_id = ci.live_revision)
         left join xowiki_page_instance on (xowiki_page_instance.page_instance_id = ci.live_revision)
    }]
  } else {
    set sql [subst {
      SELECT
         xi.package_id, xi.parent_id, xi.name,
         xi.publish_status, xi.assignee, xi.state, xi.page_template, xi.item_id,
         o.object_id, o.object_type, o.title AS object_title,
         io.context_id,
         io.creation_date,
         io.creation_user,
         io.creation_ip,
         ci.storage_type,
         o.security_inherit_p,
         o.last_modified, o.modifying_user, o.modifying_ip,
         cr.revision_id, cr.title,
         cr.content as data,
         cr_text.text,
         cr.description, cr.publish_date, cr.mime_type, cr.nls_language,
         (select xowiki_form_page_id from xowiki_form_page
          where xowiki_form_page_id = ci.live_revision) as xowiki_form_page_id,
         xowiki_page_instance.page_instance_id,
         xowiki_page_instance.instance_attributes,
         xowiki_page.page_id,
         xowiki_page.page_order,
         xowiki_page.creator
      FROM cr_text, xowiki_form_instance_item_index xi
         inner join acs_objects io on object_id = xi.item_id
         left join cr_items ci on (ci.item_id = xi.item_id)
         left join cr_revisions cr on (cr.revision_id = ci.live_revision)
         left join acs_objects o on (o.object_id = ci.live_revision)
         left join xowiki_page on (o.object_id = xowiki_page.page_id)
         left join xowiki_page_instance on (o.object_id = xowiki_page_instance.page_instance_id)
    }]
  }
  ::xo::db::require view xowiki_form_instance_item_view $sql

  # xowiki_form_instance_children:
  #
  #   Return the root_item_id and all attributes of the
  #   form_instance_item_index of all child items under the tree based
  #   on parent_ids. Use a query like the following to count the
  #   children of an item having a certain page_template (e.g.
  #   find all the folders/links/... having the specified item
  #   as parent):
  #
  #      select count(*) from xowiki_form_instance_children
  #      where root_item_id = 18255779
  #         and page_template = 20260757
  #         and publish_status='ready';
  #

  if {[db_type] eq "postgresql"} {

    ::xo::db::require view xowiki_form_instance_children {
      WITH RECURSIVE child_items AS (
        select item_id as root_item_id, * from xowiki_form_instance_item_index
      UNION ALL
        select child_items.root_item_id, xi.* from xowiki_form_instance_item_index xi, child_items
        where xi.parent_id = child_items.item_id
      )
      select * from child_items
    }

  } else {
    #
    # Oracle
    #
    ::xo::db::require view xowiki_form_instance_children {
      WITH child_items (
          root_item_id, item_id, name, package_id, parent_id, publish_status, page_template, assignee, state
      ) AS (
        select item_id as root_item_id,
               xi.item_id, xi.name, xi.package_id, xi.parent_id, xi.publish_status, xi.page_template, xi.assignee, xi.state
        from xowiki_form_instance_item_index xi
      UNION ALL
        select child_items.root_item_id,
               xi.item_id, xi.name, xi.package_id, xi.parent_id, xi.publish_status, xi.page_template, xi.assignee, xi.state
        from xowiki_form_instance_item_index xi, child_items
        where xi.parent_id = child_items.item_id
      )
      select * from child_items
    }
  }

  # xowiki_form_instance_attributes
  #
  #   Return for a given item_id the full set of attributes like the
  #   one returned from xowiki_form_instance_item_view. The idea is to
  #   make it convenient to obtain from a query all attributes
  #   necessary for creating instances. The same view can be used to
  #   complete either values from the xowiki_form_instance_item_index
  #
  #      select * from xowiki_form_instance_item_index xi
  #      left join xowiki_form_instance_attributes xa on xi.item_id = xa.item_id;
  #
  #   or from xowiki_form_instance_children
  #
  #      select * from xowiki_form_instance_children ch
  #      left join xowiki_form_instance_attributes xa on ch.item_id = xa.item_id;
  #
  #
  if {[db_type] eq "postgresql"} {

    ::xo::db::require view xowiki_form_instance_attributes {
      SELECT
         ci.item_id,
         o.package_id,
         o.object_id, o.object_type, o.title AS object_title, o.context_id,
         o.security_inherit_p, o.creation_user, o.creation_date, o.creation_ip,
         o.last_modified, o.modifying_user, o.modifying_ip,
         cr.revision_id, cr.title, content_revision__get_content(cr.revision_id) AS data,
         cr_text.text_data AS text, cr.description, cr.publish_date, cr.mime_type, cr.nls_language,
         xowiki_form_page.xowiki_form_page_id,
         xowiki_page_instance.page_instance_id,
         xowiki_page_instance.instance_attributes,
         xowiki_page.page_id,
         xowiki_page.page_order,
         xowiki_page.creator
      FROM cr_text, cr_items ci
         left join cr_revisions cr on (cr.revision_id = ci.live_revision)
         left join acs_objects o on (o.object_id = ci.live_revision)
         left join xowiki_page on (o.object_id = xowiki_page.page_id)
         left join xowiki_page_instance on (o.object_id = xowiki_page_instance.page_instance_id)
         left join xowiki_form_page on (o.object_id = xowiki_form_page.xowiki_form_page_id)
    }
  } else {
    #
    # Oracle
    #
    ::xo::db::require view xowiki_form_instance_attributes {
      SELECT
         ci.item_id,
         o.package_id,
         o.object_id, o.object_type, o.title AS object_title, o.context_id,
         o.security_inherit_p, o.creation_user, o.creation_date, o.creation_ip,
         o.last_modified, o.modifying_user, o.modifying_ip,
         cr.revision_id, cr.title, cr.content as data,
         cr_text.text, cr.description, cr.publish_date, cr.mime_type, cr.nls_language,
         xowiki_form_page.xowiki_form_page_id,
         xowiki_page_instance.page_instance_id,
         xowiki_page_instance.instance_attributes,
         xowiki_page.page_id,
         xowiki_page.page_order,
         xowiki_page.creator
      FROM cr_text, cr_items ci
         left join cr_revisions cr on (cr.revision_id = ci.live_revision)
         left join acs_objects o on (o.object_id = ci.live_revision)
         left join xowiki_page on (o.object_id = xowiki_page.page_id)
         left join xowiki_page_instance on (o.object_id = xowiki_page_instance.page_instance_id)
         left join xowiki_form_page on (o.object_id = xowiki_form_page.xowiki_form_page_id)
    }
  }

  #ns_logctl severity Debug(sql) off


  #############################
  #
  # A simple autoname handler
  #
  # The autoname handler has the purpose to generate new names based
  # on a stem and a parent_id. Typically this is used for the
  # auto-naming of FormPages. The goal is to generate "nice" names,
  # i.e. with rather small numbers.
  #
  # Instead of using the table below, another option would be to use
  # multiple sequences. However, these sequences would have dynamic
  # names, it is not clear, whether there are certain limits on the
  # number of sequences (in PostgreSQL or Oracle), the database
  # dependencies would be larger than in this simple approach.
  #
  ::xo::db::require table xowiki_autonames {
    parent_id "integer references acs_objects(object_id) ON DELETE CASCADE"
    name       varchar(3000)
    count     integer
  }
  ::xo::db::require index -table xowiki_autonames -col parent_id,name -unique true
  ::xo::db::require index -table xowiki_autonames -col parent_id

  ::xotcl::Object create autoname
  autoname proc generate {-parent_id -name} {
    ::xo::dc transaction {
      set already_recorded [::xo::dc 0or1row autoname_query {
        select count from xowiki_autonames
        where parent_id = :parent_id and name = :name}]

      if {$already_recorded} {
        incr count
        ::xo::dc dml update_autoname_counter \
            "update xowiki_autonames set count = count + 1 \
              where parent_id = :parent_id and name = :name"
      } else {
        set count 1
        ::xo::dc dml insert_autoname_counter \
            "insert into xowiki_autonames (parent_id, name, count) \
             values (:parent_id, :name, $count)"
      }
    }
    return $name$count
  }

  autoname proc basename {name} {
    # In case the provided name has an extension, return the name
    # without it.
    file rootname $name
  }

  autoname proc new {-parent_id -name} {
    while {1} {
      set generated_name [:generate -parent_id $parent_id -name $name]
      if {[::xo::db::CrClass lookup -name $generated_name -parent_id $parent_id] eq "0"} {
        return $generated_name
      }
    }
  }

  #############################
  #
  # Create the xowiki_cache
  #
  # We do here the same way as in xotcl-core/tcl/05-db-procs.tcl
  #
  # The sizes can be tailored in the config
  # file like the following:
  #
  # ns_section ns/server/${server}/acs/xowiki
  #   ns_param CacheSize                1000000
  #   ns_param CachePartitions                3
  #
  if {[catch {ns_cache flush xowiki_cache-0 NOTHING}]} {
    ns_log notice "xotcl-core: creating xowiki cache"

    ::acs::KeyPartitionedCache create ::xowiki::cache \
        -name xowiki_cache \
        -package_key xowiki \
        -parameter Cache \
        -default_size 600000 \
        -partitions 2 {
          #
          # Xowiki cache. This partitioned cache is used for various
          # caching purposes for xowiki. It is used for example for
          # caching includlets (when requested), or for full file
          # names of xowiki::File objects.  In case of bottlenecks,
          # increase the number of partitions and the cache size.
          #          
        }
  }

  #############################
  #
  # Page definitions
  #

  Page set recursion_count 0
  Page array set RE {
    include {{{([^<]+?)}}([^\}\\]|$)}
    anchor  {\\\[\\\[([^\]]+?)\\\]\\\]}
    div     {&gt;&gt;([^&<]*?)&lt;&lt;([ \n]*)?}
    clean   {[\\](\{\{|&gt;&gt;|\[\[)}
    clean2  { <br */?> *(<div)}
  }
  Page set markupmap(escape)   [list "\\\[\[" \03\01 "\\\{\{" \03\02  "\\&gt;&gt;" \03\03]
  Page set markupmap(unescape) [list \03\01 "\[\["    \03\02 "\{\{"   \03\03 "&gt;&gt;"  ]

  #
  # Templating and CSS
  #

  Page proc quoted_html_content text {
    list [ad_text_to_html -- $text] text/html
  }

  #
  # Operations on the whole instance
  #

  #
  # Page marshall/demarshall operations
  #
  # serialize_relocatable is a helper method of marshall that returns
  # relocatable objects (objects without leading colons). The
  # serialized objects will be recreated in the current namespace at
  # the target.
  #
  Page instproc serialize_relocatable {} {
    if {[::package vcompare [package require xotcl::serializer] 2.1] > -1} {
      #
      # nsf 2.1 has support for specifying the target as argument of
      # the serialize method.
      #
      set content [:serialize -target [string trimleft [self] :]]
    } else {
      #
      # Since we serialize nx and XOTcl objects, make objects the
      # old-fashioned way relocatable. This is dangerous, since it
      # might substitute as well content.
      #
      set content [:serialize]
      #
      # The following statement drops the leading colons from the object
      # names such that the imported objects are inserted into the
      # current (rather than the global) namespace. Rather than the
      # global namespace. The approach is cruel, but backward compatible
      # and avoids potential name clashes with pre-existing objects.
      #
      # Replace the first occurrence of the object name (in the alloc/create
      # statement):
      #
      regsub { ::([0-9]+) } $content { \1 } content

      #
      # Replace leading occurrences of the object name (when e.g. procs
      # are as well exported as separate statements)
      #
      regsub -all -- {\n::([0-9]+) } $content "\n\\1 " content
    }
    return $content
  }

  #
  # Page marshall
  #
  # -mode might be "export" or "copy" (latter used via clipboard)
  #
  Page instproc marshall {{-mode export}} {
    :unset_temporary_instance_variables

    set old_creation_user  [:creation_user]
    set old_modifying_user ${:modifying_user}
    set :creation_user   [:map_party -property creation_user $old_creation_user]
    set :modifying_user  [:map_party -property modifying_user $old_modifying_user]
    if {$mode eq "export" && [:is_new_entry ${:name}]} {
      #
      # For anonymous entries, names might clash in the target
      # instance. If we create on the target site for anonymous
      # entries always new instances, we end up with duplicates.
      # Therefore, we rename anonymous entries during export to
      #    ip_address:port/item_id
      #
      set server [ns_info server]
      set port [ns_config ns/server/${server}/module/nssock port]
      set new_name [ns_info address]:${port}-${:item_id}
    }

    if {[info exists new_name]} {
      #
      # We have a new name, so patch this locally to get it into the
      # serialized content.
      #
      set old_name ${:name}
      set :name $new_name
      set content [:serialize_relocatable]
      set :name $old_name
    } else {
      set content [:serialize_relocatable]
    }
    set :creation_user  $old_creation_user
    set :modifying_user $old_modifying_user

    return $content
  }

  File instproc marshall {{-mode export}} {
    set fn [:full_file_name]
    if {$mode eq "export"} {
      set :__file_content [::base64::encode [::xo::read_file $fn]]
    } else {
      set :__file_name $fn
    }
    next
  }

  Page instproc category_export {tree_name} {
    #
    # Build a command to rebuild the category tree on imports
    # (__map_command). In addition this method builds and maintains a
    # category map, which maps internal IDs into symbolic values
    # (__category_map).
    #
    # Ignore locale in get_id for now, since it seems broken
    set tree_ids [::xowiki::Category get_mapped_trees -object_id ${:package_id} \
                      -names [list $tree_name] -output tree_id]
    # Make sure to have only one tree_id, in case multiple trees are
    # mapped with the same name.
    set tree_id [lindex $tree_ids 0]
    set tree_description [dict get [category_tree::get_data $tree_id] description]
    set categories [list]
    if {![info exists :__category_map]} {
      set :__category_map [dict create]
    }
    foreach category [::xowiki::Category get_category_infos -tree_id $tree_id] {
      lassign $category category_id category_name deprecated_p level
      lappend categories $level $category_name
      set names($level$category_name
      set node_name $tree_name
      for {set l 1} {$l <= $level} {incr l} {append node_name /$names($l)}
      dict set :__category_map $category_id $node_name
    }
    set cmd [list :category_import \
                 -name $tree_name -description $tree_description \
                 -locale [lang::system::site_wide_locale] \
                 -categories $categories]
    if {![info exists :__map_command] || [string first $cmd ${:__map_command}] == -1} {
      append :__map_command \n $cmd
    }
    return ${:__category_map}
    #:log "cmd=$cmd"
  }

  Page instproc build_instance_attribute_map {form_fields} {
    #
    # Build the data structure for mapping internal values (IDs) into
    # string representations and vice versa. In particular, it builds
    # and maintains the __instance_attribute_map, which is an
    # associative list (attribute/value pairs) for form-field attributes.
    #
    #foreach f $form_fields {lappend fns [list [$f name] [$f info class]]}
    #:msg "page ${:name} build_instance_attribute_map $fns"
    if {![info exists :__instance_attribute_map]} {
      set :__instance_attribute_map [dict create]
    }
    foreach f $form_fields {
      set multiple [expr {[$f exists multiple] ? [$f set multiple] : 0}]
      #:msg "$f [$f name] cat_tree [$f exists category_tree] is fc: [$f exists is_category_field]"
      if {[$f exists category_tree] && [$f exists is_category_field]} {
        #:msg "page ${:name} field [$f name] is a category_id from [$f category_tree]"
        dict set :__instance_attribute_map [$f name] [list category [$f category_tree] $multiple]
        :category_export [$f category_tree]
      } elseif {[$f exists is_party_id]} {
        #:msg "page ${:name} field [$f name] is a party_id"
        dict set :__instance_attribute_map [$f name] [list party_id $multiple]
      } elseif {[$f istype "::xowiki::formfield::file"]} {
        dict set :__instance_attribute_map [$f name] [list file 0]
      }
    }
    return ${:__instance_attribute_map}
  }

  Page instproc category_import {-name -description -locale -categories} {
    # Execute the category import for every tree name only once per request
    set key ::__xowiki_category_import($name)
    if {[info exists $key]} return

    # ok, it is the first request
    #:msg "... catetegoy_import [self args]"

    # Do we have a tree with the specified named mapped?
    set tree_ids [::xowiki::Category get_mapped_trees -object_id ${:package_id} -locale $locale \
                      -names [list $name] -output tree_id]
    set tree_id [lindex $tree_ids 0]; # handle multiple mapped trees with same name
    if {$tree_id eq ""} {
      # The tree is not mapped, we import the category tree
      :log "...importing category tree $name"
      set tree_id [category_tree::import -name $name -description $description \
                       -locale $locale -categories $categories]
      category_tree::map -tree_id $tree_id -object_id ${:package_id}
    }

    #
    # build reverse category_map
    foreach category [::xowiki::Category get_category_infos -tree_id $tree_id] {
      lassign $category category_id category_name deprecated_p level
      lappend categories $level $category_name
      set names($level$category_name
      set node_name $name
      for {set l 1} {$l <= $level} {incr l} {append node_name /$names($l)}
      set ::__xowiki_reverse_category_map($node_name$category_id
    }
    #:msg "... catetegoy_import reverse map [array names ::__xowiki_reverse_category_map]"
    # mark the tree with this name as already imported
    set $key 1
  }


  Form instproc marshall {{-mode export}} {
    #set form_fields [:create_form_fields_from_form_constraints [:get_form_constraints]]
    #:log "--ff=$form_fields"
    # :build_instance_attribute_map $form_fields
    next
  }

  FormPage instproc map_values {map_type values} {
    # Map a list of values (for multi-valued form fields)
    # :log "map_values $map_type, $values"
    set mapped_values [list]
    foreach value $values {lappend mapped_values [:map_value $map_type $value]}
    return $mapped_values
  }

  FormPage instproc map_value {map_type value} {
    :log "map_value $map_type$value"
    if {$map_type eq "category" && $value ne ""} {
      #
      # map a category item
      #
      return [dict get ${:__category_map} $value]
    } elseif {$map_type eq "party_id" && $value ne ""} {
      #
      # map a party_id
      #
      return [:map_party -property $map_type $value]
    } elseif {$map_type eq "file" && [llength $value] % 2 == 0} {
      #
      # drop revision_id from file value
      #
      set result {}
      foreach {a v} $value {
        if {$a eq "revision_id"} continue
        lappend result $a $v
      }
      return $result
    } else {
      return $value
    }
  }

  FormPage instproc marshall {{-mode export}} {
    #
    # Handle mapping from IDs to symbolic representations in
    # form-field values. We perform the mapping on xowiki::FormPages
    # and not on xowiki::Forms, since a single xowiki::FormPages might
    # use different xowiki::Forms in its life-cycle.
    #
    # Note that only types of form-fields implied by the derived form
    # constraints are recognized. E.g. In workflows, it might be
    # necessary to move e.g. category definitions into the global form
    # constraints.
    #
    if {$mode eq "copy" && ![string match "*revision_id*" ${:instance_attributes}]} {
      return [next]
    }
    set form_fields [:create_form_fields_from_form_constraints \
                         [:get_form_constraints]]
    :build_instance_attribute_map $form_fields

    # In case we have a mapping from IDs to external values, use it
    # and rewrite instance attributes. Note that the marshalled
    # objects have to be flushed from memory later since the
    # representation of instances_attributes is changed by this
    # method.
    #
    if {[info exists :__instance_attribute_map]} {
      # :log "+++ we have an instance_attribute_map for ${:name}"
      # :log "+++ starting with instance_attributes [:instance_attributes]"
      array set multiple_index [list category 2 party_id 1 file 1]
      set ia [list]
      foreach {name value} [:instance_attributes] {
        #:log "marshall check $name $value [info exists use($name)]"
        if {[dict exists ${:__instance_attribute_map} $name]} {
          set use_name [dict get ${:__instance_attribute_map} $name]
          set map_type [lindex $use_name 0]
          set multiple [lindex $use_name $multiple_index($map_type)]
          #:log "+++ marshall check $name $value use <$use($name)> m=?$multiple"
          if {$multiple} {
            lappend ia $name [:map_values $map_type $value]
          } else {
            lappend ia $name [:map_value $map_type $value]
          }
        } else {
          # nothing to map
          lappend ia $name $value
        }
      }
      set :instance_attributes $ia
      #:log "+++ setting instance_attributes $ia"
    }
    set old_assignee [:assignee]
    set :assignee  [:map_party -property assignee $old_assignee]
    set r [next]
    set :assignee  $old_assignee
    return $r
  }

  Page instproc map_party {-property party_id} {
    if {$party_id eq "" || $party_id == 0} {
      return $party_id
    }
    ad_try {
      acs_user::get -user_id $party_id -array info
      set result [list]
      foreach a {username email first_names last_name screen_name url} {
        lappend result $a $info($a)
      }
      :log "--    map_party $party_id: $result"
      return $result
    } on error {errorMsg} {
      # swallow errors; there should be a better way to check if user
      # and or group info exists
    }
    ad_try {
      group::get -group_id $party_id -array info
      :log "got group info: [array get info]"
      set result [array get info]
      set members {}
      foreach member_id [group::get_members -group_id $party_id] {
        lappend members [:map_party -property $property $member_id]
      }
      lappend result members $members
      ns_log notice "--    map_party $party_id: $result"
      return $result
    } on error {errorMsg} {
      # swallow errors; there should be a better way to check if user
      # and or group info exists
    }
    ns_log warning "Cannot map party_id $party_id, probably not a user; property $property lost during export"
    return {}
  }

  Page instproc reverse_map_party {-entry -default_party {-create_user_ids 0}} {
    # So far, we just handle users, but we should support parties in
    # the future as well.http://localhost:8003/nimawf/admin/export

    set email [expr {[dict exists $entry email] ? [dict get $entry email] : ""}]
    if {$email ne ""} {
      set id [party::get_by_email -email $email]
      if {$id ne ""} { return $id }
    }
    set username [expr {[dict exists $entry username] ? [dict get $entry username] : ""}]
    if {$username ne ""} {
      set id [acs_user::get_by_username -username $username]
      if {$id ne ""} { return $id }
    }
    set group_name [expr {[dict exists $entry group_name] ? [dict get $entry group_name] : ""}]
    if {$group_name ne ""} {
      set id [group::get_id -group_name $group_name]
      if {$id ne ""} { return $id }
    }

    if {$create_user_ids} {
      if {$group_name ne ""} {
        :log "+++ create a new group group_name=${group_name}"
        set group_id [group::new -group_name $group_name]
        group::update -group_id $group_id [list join_policy [dict get $entry join_policy]]
        ns_log notice "+++ reverse_party_map: we could add members [dict get $entry members] - but we don't"
        return $group_id
      } else {
        :log "+++ create a new user username=${username}, email=${email}"
        set status [auth::create_user -username $username -email $email \
                        -first_names [dict get $entry first_names] \
                        -last_name [dict get $entry last_name] \
                        -screen_name [dict get $entry screen_name] \
                        -url [dict get $entry url] -nologin]
        set creation_status [dict get $status creation_status]
        if {$creation_status eq "ok"} {
          return [dict get $status user_id]
        }
        :log "+++ create user username=${username}, email=${email} failed, reason=${creation_status}"
      }
    }
    return $default_party
  }


  Page instproc reverse_map_party_attribute {-attribute {-default_party 0} {-create_user_ids 0}} {
    if {![info exists :$attribute]} {
      set :$attribute $default_party
    } elseif {[llength [set :$attribute]] < 2} {
      set :$attribute $default_party
    } else {
      set :$attribute [:reverse_map_party \
                           -entry [set :$attribute] \
                           -default_party $default_party \
                           -create_user_ids $create_user_ids]
    }
  }

  Page instproc demarshall {-parent_id -package_id -creation_user {-create_user_ids 0}} {
    # this method is the counterpart of marshall
    # Unset the context_id, which would otherwise come from the
    # original object and persisted. By default, will be set to the
    # new object's parent_id
    unset -nocomplain -- :context_id
    set :parent_id $parent_id
    set :package_id $package_id
    :reverse_map_party_attribute -attribute creation_user  \
        -default_party $creation_user -create_user_ids $create_user_ids
    :reverse_map_party_attribute -attribute modifying_user \
        -default_party $creation_user -create_user_ids $create_user_ids
    # If we import from an old database without page_order, provide a
    # default value
    if {![info exists :page_order]} {set :page_order ""}
    set is_folder_page [:is_folder_page]
    #:msg "is-folder-page ${:name} => $is_folder_page"
    if {$is_folder_page} {
      # reset names if necessary (e.g. import from old releases)
      set :name [:build_name]
    } else {
      # Check, if nls_language and lang are aligned.
      if {[regexp {^(..):} ${:name} _ lang]} {
        if {[string range [:nls_language] 0 1] ne $lang} {
          set old_nls_language [:nls_language]
          :nls_language [:get_nls_language_from_lang $lang]
          ns_log notice "nls_language for item ${:name} set from $old_nls_language to [:nls_language]"
        }
      }
    }
    # in the general case, no more actions required
    #:msg "demarshall ${:name} DONE"
  }

  File instproc demarshall {args} {
    next
    # we have to care about saving the file content

    if {[info exists :__file_content]} {
      ::xo::write_tmp_file :import_file [::base64::decode ${:__file_content}]
      unset :__file_content

    } elseif {[info exists :__file_name]} {
      set :import_file ${:__file_name}
      unset :__file_name

    } else {
      error "either __file_content or __file_name must be set"
    }
  }

  # set default values.
  # todo: with slots, it should be easier to set default values
  # for non-existing variables
  PageInstance instproc demarshall {args} {
    # some older versions do not have anon_instances and no slots
    if {![info exists :anon_instances]} {
      set :anon_instances "f"
    }
    next
  }
  Form instproc demarshall {args} {
    # Some older versions do not have anon_instances and no slots
    if {![info exists :anon_instances]} {
      set :anon_instances "t"
    }
    next
  }


  FormPage instproc reverse_map_values {-creation_user -create_user_ids map_type values category_ids_name} {
    # Apply reverse_map_value to a list of values (for multi-valued
    # form fields)
    :upvar $category_ids_name category_ids
    set mapped_values [list]
    foreach value $values {
      lappend mapped_values [:reverse_map_value \
                                 -creation_user $creation_user -create_user_ids $create_user_ids \
                                 $map_type $value category_ids]
    }
    return $mapped_values
  }

  FormPage instproc reverse_map_value {-creation_user -create_user_ids map_type value category_ids_name} {
    # Perform the inverse function of map_value. During export, internal
    # representations are exchanged by string representations, which are
    # mapped here again to internal representations
    :upvar $category_ids_name category_ids
    if {[info exists ::__xowiki_reverse_category_map($value)]} {
      #:msg "map value '$value' (category tree: $use($name)) of ${:name} to an ID"
      lappend category_ids $::__xowiki_reverse_category_map($value)
      return $::__xowiki_reverse_category_map($value)
    } elseif {$map_type eq "party_id"} {
      return [:reverse_map_party \
                  -entry $value \
                  -default_party $creation_user \
                  -create_user_ids $create_user_ids]
    } elseif {$value eq ""} {
      return ""
    } else {
      :msg "cannot map value '$value' (map_type $map_type)\
        of ${:name} to an ID; maybe there is some\
        same_named category tree with fewer entries..."
      :msg "reverse category map has values [lsort [array names ::__xowiki_reverse_category_map]]"
      return ""
    }
  }

  FormPage instproc demarshall {-parent_id -package_id -creation_user {-create_user_ids 0}} {
    # reverse map assignees
    :reverse_map_party_attribute -attribute assignee -create_user_ids $create_user_ids
    #
    # The function will compute the category_ids, which are were used
    # to categorize these objects in the source instance.
    set category_ids [list]

    #:msg "${:name} check cm=[info exists ::__xowiki_reverse_category_map] && iam=[info exists :__instance_attribute_map]"

    if {[info exists ::__xowiki_reverse_category_map]
        && [info exists :__instance_attribute_map]
      } {
      #:msg "we have a instance_attribute_map"

      #
      # replace all symbolic category values by the mapped IDs
      #
      set ia [list]
      array set use ${:__instance_attribute_map}
      array set multiple_index [list category 2 party_id 1 file 1]
      foreach {name value} [:instance_attributes] {
        #:msg "use($name) --> [info exists use($name)]"
        if {[info exists use($name)]} {
          #:msg "try to map value '$value' (category tree: $use($name))"
          set map_type [lindex $use($name) 0]
          set multiple [lindex $use($name) $multiple_index($map_type)]
          if {$multiple eq ""} {set multiple 1}
          if {$multiple} {
            lappend ia $name [:reverse_map_values \
                                  -creation_user $creation_user -create_user_ids $create_user_ids \
                                  $map_type $value category_ids]
          } else {
            lappend ia $name [:reverse_map_value \
                                  -creation_user $creation_user -create_user_ids $create_user_ids \
                                  $map_type $value category_ids]
          }
        } else {
          # nothing to map
          lappend ia $name $value
        }
      }
      set :instance_attributes $ia
      #:msg  "${:name} saving instance_attributes $ia"
    }
    set r [next]
    set :__category_ids [lsort -unique $category_ids]
    return $r
  }

  ############################################
  #
  # conditions for policy rules
  #
  ############################################
  Page instproc condition=match {query_context value} {
    #
    # Condition for conditional checks in policy rules
    # The match condition is called with an attribute
    # name and a pattern like in
    #
    #  edit {
    #     {{match {name {*weblog}}} package_id admin}
    #     {package_id write}
    #  }
    #
    # This example specifies that for a page named
    # *weblog, the method "edit" is only allowed
    # for package admins.
    #
    #:msg "query_context='$query_context', value='$value'"
    if {[llength $value] != 2} {
      error "two arguments for match required, [llength $value] passed (arguments='$value')"
    }
    ad_try {
      set success [string match [lindex $value 1] [set :[lindex $value 0]]]
    } on error {errorMsg} {
      ns_log error "error during condition match: $errorMsg"
      set success 0
    }
    return $success
  }

  Page instproc condition=regexp {query_context value} {
    #
    # Condition for conditional checks in policy rules
    # The match condition is called with an attribute
    # name and a pattern like in
    #
    #  edit               {
    #    {{regexp {name {(weblog|index)$}}} package_id admin}
    #    {package_id write}
    #  }
    #
    # This example specifies that for a page ending with
    # weblog or index, the method "edit" is only allowed
    # for package admins.
    #
    #:msg "query_context='$query_context', value='$value'"
    if {[llength $value] != 2} {
      error "two arguments for regexp required, [llength $value] passed (arguments='$value')"
    }
    ad_try {
      set success [regexp [lindex $value 1] [set :[lindex $value 0]]]
    } on error {errorMsg} {
      ns_log error "error during condition regexp: $errorMsg"
      set success 0
    }
    return $success
  }

  Page instproc condition=is_folder_page {query_context value} {
    # query_context and value are ignored
    return [:is_folder_page]
  }


  Page instproc copy_content_vars {-from_object:required {-except {}}} {
    set excluded_var {
      folder_id 1 package_id 1 absolute_links 1 lang_links 1 modifying_user 1
      publish_status 1 item_id 1 revision_id 1 last_modified 1
      parent_id 1 context_id 1
    }
    foreach var $except {
      dict set excluded_var $var 1
    }
    foreach var [$from_object info vars] {
      # don't copy vars starting with "__"
      if {[string match "__*" $var]} continue
      if {![dict exists $excluded_var $var]} {
        set :$var [$from_object set $var]
      }
    }
  }

  Page proc import {-user_id -package_id -folder_id {-replace 0} -objects} {
    ad_log_deprecated "::xowiki::Page proc" "import" "::$package_id import ..."

    if {![info exists package_id]}  {
      set package_id  [::xo::cc package_id]
    }
    set cmd [list ::$package_id import -replace $replace]

    if {[info exists user_id]} {lappend cmd -user_id $user_id}
    if {[info exists objects]} {lappend cmd -objects $objects}
    {*}$cmd
  }

  #
  # Tag management, get_tags works on instance or globally.
  #

  Page proc save_tags {
                       -package_id:required
                       -item_id:required
                       -revision_id:required
                       -user_id:required
                       tags
                     } {
    ::xo::dc dml delete_tags \
        "delete from xowiki_tags where item_id = :item_id and user_id = :user_id"

    #
    # Map funny characters in tags to white-space. Tags are just
    # single words, no quotes are allowed. The resulting tags must be
    # compatible with "Package->validate_tag".
    #
    regsub -all {[^\w.-]+} $tags " " tags

    foreach tag [split $tags " "] {
      if {$tag ne ""} {
        ::xo::dc dml insert_tag \
            "insert into xowiki_tags (item_id,package_id, user_id, tag, time) \
          values (:item_id, :package_id, :user_id, :tag, CURRENT_TIMESTAMP)"
      }
    }
    search::queue -object_id $revision_id -event UPDATE
  }

  Page proc get_tags {-package_id:required -item_id -user_id} {
    if {[info exists item_id]} {
      if {[info exists user_id]} {
        # tags for item and user
        set tags [::xo::dc list -prepare integer,integer,integer get_tags {
          SELECT distinct tag from xowiki_tags
          where user_id = :user_id and item_id = :item_id and package_id = :package_id
        }]
      } else {
        #
        # All tags for this item
        #
        set tags [::xo::dc list -prepare integer,integer get_tags {
          SELECT distinct tag from xowiki_tags
          where item_id = :item_id and package_id = :package_id
        }]
      }
    } else {
      if {[info exists user_id]} {
        #
        # All tags for this user
        #
        set tags [::xo::dc list -prepare integer,integer get_tags {
          SELECT distinct tag from xowiki_tags
          where user_id = :user_id and package_id :package_id
        }]
      } else {
        #
        # All tags for the package instance
        #
        set tags [::xo::dc list -prepare integer get_tags {
          SELECT distinct tag from xowiki_tags
          where package_id = :package_id
        }]
      }
    }
    return $tags
  }


  #
  # Methods of ::xowiki::Page
  #

  Page instforward query_parameter {%set :package_id} %proc
  Page instforward exists_query_parameter {%set :package_id} %proc
  Page instforward form_parameter {%set :package_id} %proc
  Page instforward exists_form_parameter {%set :package_id} %proc

  Page instproc get_query_parameter_return_url {{default ""}} {
    #
    # Get the return_url from query parameters and check, if this is
    # local.
    #
    set return_url [:query_parameter return_url:localurl $default]
    #if {[util::external_url_p $return_url]} {
    #  ns_log warning "return_url $return_url is apparently an external URL"
    #  ad_return_complaint 1 "Page <b>'${:name}'</b> non-local return_url was specified"
    #  ad_script_abort
    #}
    return $return_url
  }


  #   Page instproc init {} {
  #     :log "--W "
  #     ::xo::show_stack
  #     next
  #   }

  #   Page instproc destroy  {} {
  #     :log "--W "
  #     ::xo::show_stack
  #     next
  #   }

  #
  # check certain properties of a page (is_* methods)
  #

  Page ad_instproc is_folder_page {
    {-include_folder_links true}
  } {

    Check, if page is a folder.  This function is typically overlaaded
    by specializations. Plain xowiki::Pages are never folders.

    @param include_folder_links return true, if the current page is a
           link to a folder.

    @return boolean
  } {
    return 0
  }

  FormPage ad_instproc is_folder_page {
    {-include_folder_links true}
  } {
    Check, if FormPage is a folder. A FormPage is a folder when its
    page template is the folder.form or if this is a link pointing to
    a folder.

    @param include_folder_links return true, if the current page is a
           link to a folder.
    @return boolean
  } {
    #
    # Make sure, the page_template is instantiated
    #
    if {![nsf::is object ::${:page_template}]} {
      ::xo::db::CrClass get_instance_from_db -item_id ${:page_template}
    }
    set page_template_name [${:page_template} name]
    if {$page_template_name eq "en:folder.form"} {
      return 1
    } elseif {$include_folder_links && $page_template_name eq "en:link.form"} {
      set link_type [:get_property_from_link_page link_type]
      return [expr {$link_type eq "folder_link"}]
    } else {
      return 0
    }
  }

  #
  # Check, if a page is a link
  #
  Page instproc is_link_page {} {
    return 0
  }

  FormPage instproc is_link_page {} {
    #
    # Make sure, the page_template is instantiated
    #
    if {![nsf::is object ::${:page_template}]} {
      ::xo::db::CrClass get_instance_from_db -item_id ${:page_template}
    }
    return [expr {[${:page_template} name] eq "en:link.form"}]
  }

  Page instproc is_unprefixed {} {
    #
    # Pages which should not get an extra language prefix.  In case,
    # your package has further such requirements, extend this proc in
    # you package.
    #
    return [expr {[:is_folder_page]
                  || [:is_link_page]
                  || ${:name} eq ${:revision_id}
                }]
  }

  #
  # link properties
  #
  Page instproc get_property_from_link_page {property {default ""}} {

    if {![:is_link_page]} {
      return $default
    }

    set item_ref [:property link]
    ::xo::db::CrClass get_instance_from_db -item_id ${:item_id}
    set props [::xo::cc cache [list ::${:item_id} compute_link_properties $item_ref]]

    if {[dict exists $props $property]} {
      #${:item_id} msg "prop $property ==> [dict get $props $property]"
      return [dict get $props $property]
    }
    return $default
  }

  Page instproc get_target_from_link_page {{-depth 10}} {
    #
    # Dereference link and return target object of the
    # link. Dereferencing happens up to a maximal depth to avoid loop
    # in circular link structures. If this method is called with e.g.
    # {-depth 1} and the link (actual object) points to some link2,
    # the link2 is returned.
    #
    # @param depth maximal dereferencing depth
    # @return target object or empty
    #
    set item_id [:get_property_from_link_page item_id 0]
    if {$item_id == 0} {return ""}
    set target [::xo::db::CrClass get_instance_from_db -item_id $item_id]
    set target_package_id [$target package_id]
    if {$target_package_id != ${:package_id}} {
      ::xowiki::Package require $target_package_id
      #::xowiki::Package initialize -package_id $target_package_id -init_url false -keep_cc true
    }
    if {$depth > 1 && [$target is_link_page]} {
      set target [:get_target_from_link_page -count [expr {$depth - 1}]]
    }
    return $target
  }

  FormPage instproc compute_link_properties {item_ref} {
    set package_id ${:package_id}
    set page [::$package_id get_page_from_item_ref \
                  -default_lang [:lang] \
                  -parent_id ${:parent_id} \
                  $item_ref]
    if {$page ne ""} {
      set item_id [$page item_id]
      set link_type [expr {[$page is_folder_page] ? "folder_link" : "link"}]
      set cross_package [expr {$package_id != [$page package_id]}]
    } else {
      set item_id 0
      set link_type "unresolved"
      set cross_package 0
    }
    #:msg [list item_ref $item_ref item_id $item_id link_type $link_type cross_package $cross_package]
    return [list item_ref $item_ref item_id $item_id link_type $link_type cross_package $cross_package]
  }

  #
  # Check, if a page is a form
  #

  Page instproc is_form {} {
    return 0
  }
  Form instproc is_form {} {
    return 1
  }
  FormPage instproc is_form {} {
    return [:exists_property form_constraints]
  }

  FormPage instproc hstore_attributes {} {
    # Per default, we save all instance attributes in hstore, but a
    # subclass/object might have different requirements.
    return ${:instance_attributes}
  }

  #
  # Update helper for xowiki_form_instance_item_index (called from
  # cr_procs, whenever a live-revision becomes updated).
  #
  FormPage ad_instproc update_item_index {} {

    Tailored version of CrItem.update_item_index to keep
    insert_xowiki_form_instance_item_index in sync after updates.

  } {
    :instvar name item_id package_id parent_id publish_status \
        page_template instance_attributes assignee state

    set useHstore [::$package_id get_parameter use_hstore:boolean 0]
    set updateVars {name = :name, package_id = :package_id,
      parent_id = :parent_id, publish_status = :publish_status,
      page_template = :page_template, assignee = :assignee,
      state = :state}

    if {$useHstore} {
      set hkey [::xowiki::hstore::dict_as_hkey [:hstore_attributes]]
      append updateVars ", hkey = '$hkey'"
    }

    set rows [xo::dc dml update_xowiki_form_instance_item_index [subst {
      update xowiki_form_instance_item_index
      set $updateVars
      where item_id = :item_id
    }]]

    if {$rows ne "" && $rows < 1} {
      set insertVars {item_id, name, package_id, parent_id, publish_status,
        page_template, assignee, state
      }
      set insertValues {:item_id, :name, :package_id, :parent_id, :publish_status,
        :page_template, :assignee, :state
      }
      if {$useHstore} {
        append insertVars {, hkey}
        append insertValues ", '$hkey'"
      }

      ::xo::dc dml insert_xowiki_form_instance_item_index [subst {
        insert into xowiki_form_instance_item_index
        ($insertVars) values ($insertValues)
      }]
    }
  }

  FormPage ad_instproc update_attribute_from_slot {-revision_id slot:object value} {

    Tailored version of update_attribute_from_slot to keep
    insert_xowiki_form_instance_item_index in sync after single
    attribute updates.

  } {
    #
    # Perform first the regular operations.
    #
    next
    #
    # Make sure to update update_item_index when the attribute is
    # contained in the xowiki_form_instance_item_index.
    #
    set colName [$slot column_name]

    if {$colName in {
      package_id
      parent_id
      publish_status
      page_template
      assignee
      state
    }} {
      ::xowiki::update_item_index -item_id ${:item_id} -$colName $value
    } elseif {
              $colName eq "instance_attributes"
              && [::xo::dc has_hstore]
              && [::${:package_id} get_parameter use_hstore:boolean 0]
            } {
      ::xowiki::update_item_index -item_id ${:item_id} -hstore_attributes $value
    }
  }

  d_proc update_item_index {
    -item_id:required
    -name
    -package_id
    -parent_id
    -publish_status
    -page_template
    -assignee
    -state
    -hstore_attributes
  } {

    Helper function to update single or multiple fields of the
    xowiki_form_instance_item_index. Call this function only when
    updating fields of the xowiki_form_instance_item_index in cases
    where the standard API based on save and save_use can not be used.

  } {
    set updates {}
    foreach var {
      name package_id parent_id
      publish_status page_template
      assignee state
    } {
      if {[info exists $var]} {
        lappend updates "$var = :$var"
      }
    }
    if {[info exists hstore_attributes]} {
      set hkey [::xowiki::hstore::dict_as_hkey $hstore_attributes]
      lappend updates "hkey = '$hkey'"
    }
    if {[llength $updates] > 0} {
      set setclause [join $updates ,]
      xo::dc dml update_xowiki_form_instance_item_index [subst {
        update xowiki_form_instance_item_index
        set $setclause
        where item_id = :item_id
      }]
    }
  }

  #
  # Define a specialized version of CrClass.fetch_object based
  # on xowiki_form_instance_item_view
  #
  FormPage ad_proc fetch_object {
    -item_id:required
    {-revision_id 0}
    -object:required
    {-initialize:boolean true}
  } {
    Load a content item into the specified object. If revision_id is
    provided, the specified revision is returned, otherwise the live
    revision of the item_id. If the object does not exist, we create it.

    @return cr item object
  } {
    #ns_log notice "=== fetch_object $item_id"
    #
    # We handle here just loading object instances via item_id, since
    # only live_revisions are kept in xowiki_form_instance_item_index.
    # The loading via revision_id happens as before in CrClass.
    #
    if {$item_id == 0} {
      return [next]
    }

    if {![nsf::is object $object]} {
      # if the object does not yet exist, we have to create it
      :create $object
    }

    db_with_handle db {
      set sql [::xo::dc prepare -handle $db -argtypes integer {
        select * from xowiki_form_instance_item_view where item_id = :item_id
      }]
      set selection [db_exec 0or1row $db dbqd..Formpage-fetch_object $sql]
    }

    if {$selection eq ""} {
      error [subst {
        The form page with item_id $item_id was not found in the
        xowiki_form_instance_item_index.  Consider 'DROP TABLE
        xowiki_form_instance_item_index CASCADE;' and restart server
        (the table is rebuilt automatically, but this could take a
        while, when the number of pages is huge).
      }]
    }

    $object mset [ns_set array $selection]

    if {$initialize} {
      $object initialize_loaded_object
    }
    return $object
  }

  #
  # Define a specialized version of CrItem.set_live_revision updating the item index.
  #

  FormPage ad_instproc set_live_revision {-revision_id:required {-publish_status "ready"}} {
    @param revision_id
    @param publish_status one of 'live', 'ready' or 'production'
  } {
    next

    # Fetch fresh instance from db so that we have actual values
    # from the live revision for the update of the item_index.

    set page [::xo::db::CrClass get_instance_from_db -revision_id $revision_id]
    $page publish_status $publish_status
    $page update_item_index
  }

  #
  # helper for nls and lang
  #

  Page instproc lang {} {
    return [string range ${:nls_language} 0 1]
  }

  Page instforward get_nls_language_from_lang ::xowiki::Package %proc

  Page instproc build_name {{-nls_language ""}} {
    #
    # Build the name of the page, based on the provided nls_language
    # This method strips existing language-prefixes and uses the
    # provided nls_language or the instance variable for the new name.
    # It handles as well anonymous pages, which are never equipped
    # with language prefixes. ::xowiki::File has its own method.
    #
    set name ${:name}
    set stripped_name $name
    regexp {^..:(.*)$} $name _ stripped_name

    #:log "$name / '$stripped_name'"
    # prepend the language prefix only, if the entry is not empty
    if {$stripped_name ne ""} {
      if {[:is_folder_page] || [:is_link_page]} {
        #
        # Do not add a language prefix to folder pages
        #
        set name $stripped_name
      } else {
        if {$nls_language ne ""} {
          set :nls_language $nls_language
        }
        set name [:lang]:$stripped_name
      }
    }
    return $name
  }

  #
  # Resolve context handling.
  #
  Page instproc set_resolve_context {-package_id:required -parent_id:required -item_id} {
    #
    # Push the last values to the stack
    #
    set stack_entry [list -package_id ${:package_id} -parent_id ${:parent_id} -item_id ${:item_id}]
    lappend :resolve_context_stack $stack_entry

    #
    # Reset the current values with the specified ones
    #
    if {${:parent_id} != $parent_id} {
      if {![info exists :physical_parent_id]} {
        set :physical_parent_id ${:parent_id}
      }
      set :parent_id $parent_id
    }
    if {${:package_id} != $package_id} {
      if {![info exists :physical_package_id]} {
        set :physical_package_id ${:package_id}
      }
      set :package_id $package_id
      #:msg "doing extra require on ${:physical_package_id}"
      #::xowiki::Package require ${:physical_package_id}
    }
    if {[info exists item_id] && ${:item_id} != $item_id} {
      if {![info exists :physical_item_id]} {
        set :physical_item_id ${:item_id}
      }
      set :item_id $item_id
    }
  }

  Page instproc reset_resolve_context {} {
    #
    # Pop the last values from the stack
    #
    if {![info exists :resolve_context_stack] || [llength ${:resolve_context_stack}] < 1} {
      error "set_resolve_context and reset_resolve_context calls not balanced"
    }
    set entry [lindex ${:resolve_context_stack} end]
    set :resolve_context_stack [lrange ${:resolve_context_stack} 0 end-1]
    :configure {*}$entry
    #
    # When the stack is empty, remove the stack and the "physical*" attributes
    #
    if {[llength ${:resolve_context_stack}] == 0} {
      unset :resolve_context_stack
      foreach att {item package parent} {
        set name physical_${att}_id
        if {[info exists :$name]} {
          unset :$name
        }
      }
    }
  }

  Page instproc physical_parent_id {} {
    if {[info exists :physical_parent_id]} {
      return ${:physical_parent_id}
    } else {
      return ${:parent_id}
    }
  }

  Page instproc physical_package_id {} {
    if {[info exists :physical_package_id]} {
      return ${:physical_package_id}
    } else {
      return ${:package_id}
    }
  }
  Page instproc physical_item_id {} {
    if {[info exists :physical_item_id]} {
      return ${:physical_item_id}
    } else {
      return ${:item_id}
    }
  }

  #
  # folder handling
  #

  Page instproc get_folder {-folder_form_ids:required} {
    set page [self]
    while {1} {
      if {[$page istype ::xowiki::FormPage]} {
        if {[$page is_folder_page]} break

        #     set page_template [$page page_template]
        #     set page_template_name [$page_template name]
        #         # search the page_template in the list of form_ids
        #         if {$page_template in $folder_form_ids} {
        #           break
        #     } elseif {$page_template_name eq "en:folder.form"} {
        #       # safety belt, in case we have in different directories
        #       # different en:folder.form
        #       break
        #     } elseif {$page_template_name eq "en:link.form"} {
        #       set fp [:is_folder_page]
        #       :msg fp=$fp
        #       break
        #         }
      }
      set page [::xo::db::CrClass get_instance_from_db -item_id [$page parent_id]]
    }
    return $page
  }

  #
  # save / restore
  #

  Page instproc can_contain {obj} {
    #
    # This is a stub which can / should be refined in applications,
    # which want to disallow pages (e.g. folders) to be parent of some
    # kind of content. The function should return 0 if some content is
    # not allowed.
    #
    return 1
  }

  Page instproc can_link {item_id} {
    #
    # This is a stub which can / should be refined in applications,
    # which want to disallow links to other pages, in the sense, that
    # the links are not shown at all. A sample implementation might
    # look like the following.
    #
    # if {$item_id ne 0} {
    #   set obj [::xo::db::CrClass get_instance_from_db -item_id $item_id]
    #   return [$obj can_be_linked]
    # }
    #
    return 1
  }

  Page instproc can_be_linked {} {
    return 1
  }

  Page instproc stats_record_count {name} {
    # This is a stub which can / should be overloaded in applications,
    # collecting statistics about certain usage pattern (e.g. exam
    # workflows).  This method is overloaded in xowf, and is here just
    # for safety reasons to avoid hard errors.
    ns_log error "the method Page->stats_record_count should not be called"
  }

  Page instproc stats_record_detail args {
    # This is a stub which can / should be overloaded in applications,
    # collecting statistics about certain usage pattern (e.g. exam
    # workflows).  This method is overloaded in xowf, and is here just
    # for safety reasons to avoid hard errors.
    ns_log error "the method Page->stats_record_detail should not be called"
  }

  Page instproc can_save {} {
    #
    # Determine the parent object of the page to be saved. If the
    # parent object is a page as well, then call can_contain. The
    # function is just determining a Boolean value such it can be used
    # for testing insertability as well.
    #
    set parent [:get_parent_object]
    if {$parent ne "" && [$parent istype ::xowiki::Page]} {
      return [$parent can_contain [self]]
    }
    return 1
  }

  Page instproc evaluate_form_field_condition {cond} {
    #
    # Can be refined
    #
    return 0
  }

  Page instproc save args {
    if {![:can_save]} {error "can't save this page under this parent"}
    ${:package_id} flush_page_fragment_cache
    set id [next]
    :check_unresolved_references
    return $id
  }

  Page instproc save_new args {
    if {![:can_save]} {error "can't save this page under this parent"}
    ${:package_id} flush_page_fragment_cache
    set id [next]
    :check_unresolved_references
    return $id
  }

  Page instproc initialize_loaded_object {} {
    if {[info exists :title] && ${:title} eq ""} {set :title ${:name}}
    next
  }

  #
  # misc
  #

  Page instproc get_parent_object {} {
    #
    # Obtain the parent object for a page. If the parent page is a
    # dummy entry or not an object, return empty.
    #
    set parent_id ${:parent_id}
    if {$parent_id > 0} {
      if {! [nsf::is object ::$parent_id] } {
        ::xo::db::CrClass get_instance_from_db -item_id $parent_id
      }
      return ::$parent_id
    }
    return ""
  }

  Page instproc get_instance_attributes {} {
    if {[info exists :instance_attributes]} {
      return ${:instance_attributes}
    }
    return ""
  }

  Page instproc update_publish_status {new_publish_status} {
    #
    # The publish_status of xowiki is used for "advertising"
    # pages. When the publish_status is e.g. in "production" or
    # "expired", users can access this object when they know obout its
    # existence (e.g. workflow assignments), but it is excluded from
    # listings, which contain - per default - only elements in
    # publish_status "ready".
    #
    # This proc can be used to change the publish status of a page and
    # handle visibility via syndication.
    #
    if {$new_publish_status ni {production ready live expired}} {
      error "update_publish_status receives invalid publish status '$new_publish_status'"
    }

    if {$new_publish_status ne ${:publish_status}} {
      :set_live_revision \
          -revision_id ${:revision_id} \
          -publish_status $new_publish_status

      ::xo::xotcl_object_cache flush ${:revision_id}
      ::xo::xotcl_object_cache flush ${:item_id}

      if {$new_publish_status eq "ready"} {
        ::xowiki::notification::do_notifications -revision_id ${:revision_id}
        ::xowiki::datasource -nocleanup ${:revision_id}
      } else {
        set revision_id ${:revision_id}
        ::xo::dc dml -prepare integer flush_syndication {delete from syndication where object_id = :revision_id}
      }
    }
  }

  #
  # render and substitutions
  #

  Page instproc regsub_eval {{-noquote:boolean false} re string cmd {prefix ""}} {
    if {$noquote} {
      set map { \[ \\[ \] \\] \$ \\$ \\ \\\\}
    } else {
      set map { \" \\\" \[ \\[ \] \\] \$ \\$ \\ \\\\}
    }
    uplevel [list subst [regsub -all -- $re [string map $map $string"\[$cmd\]"]]
  }

  Page instproc error_during_render {msg} {
    return "<div class='errorMsg'>$msg</div>"
  }

  Page instproc error_in_includelet {arg msg} {
    return [:error_during_render "[_ xowiki.error_in_includelet [list arg $arg name ${:name}]]<br >\n$msg"]
  }

  Page ad_instproc resolve_included_page_name {page_name} {
    Determine the page object for the specified page name.
    The specified page name might have the form
    //some_other_instance/page_name, in which case the
    page is resolved from some other package instance.
    If the page_name does not contain a language prefix,
    the language prefix of the including page is used.
  } {
    if {$page_name ne ""} {
      set page [::${:package_id} resolve_page_name_and_init_context -lang [:lang] $page_name]
      if {$page eq ""} {
        error "Cannot find page '$page_name' to be included in page '${:name}'"
      }
    } else {
      set page [self]
    }
    return $page
  }

  Page instproc instantiate_includelet {arg} {
    # we want to use package_id as proc-local variable, since the
    # cross package reference might alter it locally
    set package_id ${:package_id}

    # do we have a wellformed list?
    ::try {
      set page_name [lindex $arg 0]
    } on error {errMsg} {
      # there must be something syntactically wrong
      return [:error_in_includelet $arg [_ xowiki.error-includelet-dash_syntax_invalid]]
    }
    #:msg "includelet: [lindex $arg 0], caller parameters ? '[lrange $arg 1 end]'"

    # the include is either a includelet class, or a wiki page
    if {[:isclass ::xowiki::includelet::$page_name]} {
      # direct call, without page, not tailorable
      set page [::xowiki::includelet::$page_name new \
                    -package_id $package_id \
                    -name $page_name \
                    -locale [::xo::cc locale] \
                    -actual_query [::xo::cc actual_query]]
    } else {
      #
      # Include a wiki page, tailorable.
      #
      #set page [:resolve_included_page_name $page_name]
      set page [::$package_id get_page_from_item_ref \
                    -use_package_path true \
                    -use_site_wide_pages true \
                    -use_prototype_pages true \
                    -default_lang [:lang] \
                    -parent_id ${:parent_id} $page_name]

      if {$page ne "" && ![$page exists __decoration]} {
        #
        # we use as default decoration for included pages
        # the "portlet" decoration
        #
        $page set __decoration [::$package_id get_parameter default-portlet-decoration:graph portlet]
      }
    }

    if {$page ne ""} {
      $page set __caller_parameters [lrange $arg 1 end]
      $page destroy_on_cleanup
      set :__last_includelet $page
      $page set __including_page [self]
      if {[$page istype ::xowiki::Includelet]} {
        $page initialize
      }
    }
    return $page
  }

  Page instproc render_includelet {includelet} {
    #
    # The passed includelet is either an instance of ::xowiki::Page or
    # of ::xowiki::Includelet
    #
    foreach {att value} [$includelet set __caller_parameters] {
      switch -- $att {
        -decoration {$includelet set __decoration $value}
        -title {$includelet set title $value}
        -id {$includelet set id $value}
      }
    }
    if {[$includelet exists __decoration] && [$includelet set __decoration] ne "none"} {
      $includelet mixin add ::xowiki::includelet::decoration=[$includelet set __decoration]
    }

    set includeletClass [$includelet info class]
    if {[$includeletClass exists cacheable] && [$includeletClass set cacheable]} {
      $includelet mixin add ::xowiki::includelet::page_fragment_cache
    }

    if {[$includelet istype ::xowiki::Includelet]} {
      # call this always
      $includelet include_head_entries
    }

    # "render" might be cached
    set html ""
    ad_try {
      set html [$includelet render]
    } on error {errorMsg} {
      set errorCode $::errorCode
      set errorInfo $::errorInfo
      if {[string match "*for parameter*" $errorMsg]} {
        ad_return_complaint 1 [ns_quotehtml $errorMsg]
        ad_script_abort
      } else {
        ad_log error "render_includelet $includeletClass led to: $errorMsg ($errorCode)\n$errorInfo"
        set page_name [$includelet name]
        set ::errorInfo [::xowiki::Includelet html_encode $errorInfo]
        set html [:error_during_render [_ xowiki.error-includelet-error_during_render]]
      }
    }
    #:log "--include includelet returns $html"
    return $html
  }

  #   Page instproc include_portlet {arg} {
  #     :log "+++ method [self proc] of [self class] is deprecated"
  #     return [:include $arg]
  #   }

  Page ad_instproc include {-configure arg} {
    Include the html of the includelet. The method generates
    an includelet object (might be another xowiki page) and
    renders it and returns either html or an error message.
  } {
    set page [:instantiate_includelet $arg]
    if {$page eq "" || ![nsf::is object $page]} {
      # The variable 'page_name' is required by the message key
      set page_name [ns_quotehtml $arg]
      return [:error_during_render [_ xowiki.error-includelet-unknown]]
    }
    if {[$page istype ::xowiki::Page]} {
      set package_id [$page package_id]
      set allowed [[::$package_id set policy] check_permissions \
                       -package_id $package_id \
                       -user_id [::xo::cc set untrusted_user_id] \
                       $page view]
      if {!$allowed} {
        return "<div class='errorMsg'>Insufficient privileges to view content of [$page name].</div>"
      }
    }
    if {[info exists configure]} {
      $page configure {*}$configure
    }
    return [:render_includelet $page]
  }

  Page instproc check_adp_include_path { adp_fn } {
    #
    # For security reasons, don't allow arbitrary paths to different
    # packages.  All allowed includelets must be made available
    # under xowiki/www (preferable xowiki/lib/portlets/*). When the
    # provided path contains "admin/*", admin rights are required.
    #
    if {[string match "admin/*" $adp_fn]} {
      set allowed [::xo::cc permission \
                       -object_id ${:package_id} -privilege admin \
                       -party_id [::xo::cc user_id]]
      if {!$allowed} {
        return [list allowed $allowed msg "Page can only be included by an admin!" fn ""]
      }
    }
    if {[regexp {^/www/templates/(.*)$} $adp_fn . widget]} {
       return [list allowed 1 msg "" fn /packages/openacs-bootstrap3-theme/resources/widgets/$widget]
    }
    if {[string match "/*" $adp_fn] || [string match "../*" $adp_fn]} {
      # Never allow absolute paths.
      #
      # Alternatively, we could allow url-based includes, and then using
      # set node [site_node::get -url [ad_conn url]]
      # permission::require_permission -object_id $node(object_id) -privilege read
      # ... or admin/* based checks like in rp.
      #
      return [list allowed 0 msg "Invalid name for adp_include" fn ""]
    }
    return [list allowed 1 msg "" fn /packages/[::${:package_id} package_key]/lib/$adp_fn]
  }

  Page instproc include_content {arg ch2} {
    #
    # Recursion depth is a global variable to ease the deletion etc.
    #
    if {[incr ::xowiki_inclusion_depth] > 10} {
      return [:error_in_includelet $arg [_ xowiki.error-includelet-nesting_to_deep]]$ch2
    }
    if {[regexp {^adp (.*)$} $arg _ adp]} {
      try {
        lindex $adp 0
      } on error {errMsg} {
        # there is something syntactically wrong
        incr ::xowiki_inclusion_depth -1
        return [:error_in_includelet $arg [_ xowiki.error-includelet-adp_syntax_invalid]]$ch2
      }
      set adp [string map {&nbsp; " "$adp]

      #
      # Check the provided name of the adp file
      #
      set path_info [:check_adp_include_path [lindex $adp 0]]
      #:log "path_info returned $path_info"
      if {![dict get $path_info allowed]} {
        incr ::xowiki_inclusion_depth -1
        return [:error_in_includelet $arg [dict get $path_info msg]]$ch2
      }

      set adp_fn [dict get $path_info fn]
      #
      # check the provided arguments
      #
      set adp_args [lindex $adp 1]
      if {[llength $adp_args] % 2 == 1} {
        incr ::xowiki_inclusion_depth -1
        set adp $adp_args
        incr ::xowiki_inclusion_depth -1
        return [:error_in_includelet $arg [_ xowiki.error-includelet-adp_syntax_invalid]]$ch2
      }

      lappend adp_args __including_page [self]
      set including_page_level [template::adp_level]
      ad_try {
        set page [template::adp_include $adp_fn $adp_args]
      } on error {errorMsg} {
        ad_log error "$errorMsg\n$::errorInfo"
        # in case of error, reset the adp_level to the previous value
        set ::template::parse_level $including_page_level
        return [:error_in_includelet $arg \
                    [_ xowiki.error-includelet-error_during_adp_evaluation]]$ch2
      } finally {
        incr ::xowiki_inclusion_depth -1
      }

      return $page$ch2
    } else {
      # we have a direct (adp-less include)
      set html [:include [:unescape $arg]]
      #:log "--include includelet returns $html"
      incr ::xowiki_inclusion_depth -1
      return $html$ch2
    }
  }

  Page instproc div {arg} {
    if {$arg eq "content"} {
      return "<div id='content' class='column'>"
    } elseif {[string match "left-col*" $arg] \
                  || [string match "right-col*" $arg] \
                  || $arg eq "sidebar"} {
      return "<div id='[ns_quotehtml $arg]' class='column'>"
    } elseif {$arg eq "box"} {
      return "<div class='box'>"
    } elseif {$arg eq ""} {
      return "</div>"
    } else {
      return ""
    }
  }

  Page instproc unescape string {
    # Some browsers change {{cmd -flag "..."}} into {{cmd -flag &quot;...&quot;}}
    # We have to change this back
    return [string map [list "&gt;" > "&lt;" < "&quot;" \" "&amp;" & "&semicolon;" {;} ] $string]
  }

  Page instproc get_anchor_and_query {link} {
    #
    # strip anchor and query from link name
    #
    set anchor ""
    set query ""
    # remove anchor
    regexp {^([^#]*)(\#|%23)(.*)$} $link _ link . anchor
    # remove query part
    regexp {^(.*)[?]([^?]+)$} $link _ link query
    return [list link $link anchor $anchor query $query]
  }

  Page instproc normalize_internal_link_name {name stripped_name lang} {
    #
    # strip anchor and query from link name
    #
    set anchor ""
    set query ""
    # remove anchor
    regexp {^([^#]*)(\#|%23)(.*)$} $stripped_name _ stripped_name . anchor
    # remove query part
    regexp {^(.*)[?]([^?]+)$} $stripped_name _ stripped_name query

    # if we have an empty stripped name, it is a link to the current
    # page, maybe in a different language
    if {$stripped_name eq ""} {
      regexp {:([^:]+)$} $name _ stripped_name
    }

    set normalized_name [::${:package_id} normalize_name $stripped_name]
    #:msg "input: [self args] - lang=[:lang], [:nls_language]"
    if {$lang  eq ""}   {set lang [:lang]}
    if {$name  eq ""}   {set name $lang:$normalized_name}
    #:msg result=[list name $name lang $lang normalized_name $normalized_name anchor $anchor]
    return [list name $name lang $lang normalized_name $normalized_name anchor $anchor query $query]
  }

  #
  # Forwarder to the Package instance object
  #
  Page instforward item_ref {%my package_id} %proc
  Page instforward get_ids_for_bulk_actions {%my package_id} %proc

  Page ad_instproc pretty_link {
    {-anchor ""}
    {-query ""}
    {-absolute:boolean false}
    {-siteurl ""}
    {-lang ""}
    {-download false}
    {-path_encode:boolean true}
  } {
    This method is a convenience stub for Package->pretty_link
    and can be overloaded for different pages types.

    Note that it is necessary to initialize the package before this
    method can be used.

    @param anchor anchor to be added to the link
    @param query query parameters to be added literally to the resulting URL
    @param absolute make an absolute link (including protocol and host)
    @param lang use the specified 2 character language code (rather than computing the value)
    @param download create download link (without m=download)
    @param path_encode control encoding of the url path. Returns the URL path urlencoded,
    unless path_encode is set to false.

    @return the pretty_link for the current page
    @see ::xowiki::Package instproc pretty_link
  } {
    if {${:parent_id} eq "0"} {
      set msg "you must not call pretty_link on a page with parent_id 0"
      ad_log error $msg
      error $msg
    }
    ${:package_id} pretty_link -parent_id ${:parent_id} \
        -anchor $anchor -query $query -absolute $absolute -siteurl $siteurl \
        -lang $lang -download $download -page [self] \
        -path_encode $path_encode \
        ${:name}
  }

  Page instproc detail_link {} {
    if {[info exists :instance_attributes]} {
      if {[dict exists ${:instance_attributes} detail_link]
          && [dict get ${:instance_attributes} detail_link] ne ""} {
        return [dict get ${:instance_attributes} detail_link]
      }
    }
    return [:pretty_link]
  }

  Page instproc self_link_ids {} {
    set parent_id [expr {[info exists :__ignore_self_in_links] ? ${:parent_id} : [:physical_item_id]}]
    return [list package_id [:physical_package_id] parent_id $parent_id]
  }

  Page instproc create_link {arg} {
    #:msg [self args]
    set label $arg
    set link $arg
    set options ""
    regexp {^([^|]+)[|](.*)$} $arg _ link label
    regexp {^([^|]*)[|](.*)$} $label _ label options
    set options [:unescape $options]
    set link [string trim $link]

    # Get the package_id from the provided path, and - if found -
    # return the shortened link relative to it.
    set package_id [::${:package_id} resolve_package_path $link link]
    if {$package_id == 0} {
      # we treat all such links like external links
      if {[regsub {^//} $link / link]} {
        #
        # For local links (starting with //), we provide
        # a direct treatment. JavaScript and CSS files are
        # included, images are rendered directly.
        #
        switch -glob -- [::xowiki::guesstype $link] {
          text/css {
            ::xo::Page requireCSS $link
            return ""
          }
          application/x-javascript -
          application/javascript {
            ::xo::Page requireJS $link
            return ""
          }
          image/* {
            Link create [self]::link \
                -page [self] \
                -name "" \
                -type localimage \
                -label $label \
                -href $link
            [self]::link configure {*}$options
            return [self]::link
          }
        }
      }
      set l [ExternalLink new -label $label -href $link]
      $l configure {*}$options
      return $l
    }

    #
    # TODO missing: typed links
    #
    ## do we have a typed link? prefix has more than two chars...
    #  if {[regexp {^([^:/?][^:/?][^:/?]+):((..):)?(.+)$} $link _ \
        # link_type _ lang  stripped_name]} {
    # set name file:$stripped_name
    #  }

    set link_info [:get_anchor_and_query $link]
    set parent_id [expr {$package_id == ${:package_id} ?
                         ${:parent_id} : [::$package_id folder_id]}]

    # we might consider make this configurable
    set use_package_path true
    set is_self_link false

    if {[regexp {^:(..):(.+)$} [dict get $link_info link] _ lang stripped_name]} {
      #
      # a language link (it starts with a ':')
      #
      set item_ref_info [::$package_id item_ref \
                             -use_package_path $use_package_path \
                             -default_lang [:lang] \
                             -parent_id $parent_id \
                             ${lang}:$stripped_name]
      dict set item_ref_info link_type language

    } elseif {[regexp {^[.]SELF[.]/(.*)$} [dict get $link_info link] _ link]} {
      #
      # Remove ".SELF./" from the path and search for the named
      # resource (e.g. the image name) under the current (physical)
      # item.
      #
      set self_link_ids [:self_link_ids]
      set parent_id  [dict get $self_link_ids parent_id]
      set package_id [dict get $self_link_ids package_id]

      #ns_log notice "SELF-LINK '[dict get $link_info link]' in TEXT resolve with parent $parent_id"
      set is_self_link true
      set item_ref_info [::$package_id item_ref \
                             -use_package_path $use_package_path \
                             -default_lang [:lang] \
                             -parent_id $parent_id \
                             $link]
      dict set link_info link $link
      #:log "SELF-LINK returns $item_ref_info"

    } else {
      #
      # A plain link, search relative to the parent.
      #
      #ns_log notice "PLAIN-LINK '[dict get $link_info link]' in TEXT resolve with parent $parent_id"
      set item_ref_info [::$package_id item_ref \
                             -use_package_path $use_package_path \
                             -default_lang [:lang] \
                             -parent_id $parent_id \
                             [dict get $link_info link]]
    }
    #ns_log notice "link_info $link_info"
    #ns_log notice "--L link <$arg> lang [:lang] CURRENT ${:name} nls_lang ${:nls_language} -> item_ref_info $item_ref_info"

    #:log "link '[dict get $link_info link]' package_id $package_id ${:package_id} => [array get {}]"

    if {$label eq $arg} {
      set label [dict get $link_info link]
    }

    set item_name [string trimleft [dict get $item_ref_info prefix]:[dict get $item_ref_info stripped_name] :]
    Link create [self]::link \
        -page [self] \
        -form [dict get $item_ref_info form] \
        -type [dict get $item_ref_info link_type] \
        -name $item_name \
        -lang [dict get $item_ref_info prefix] \
        -anchor [dict get $link_info anchor] \
        -query [dict get $link_info query] \
        -stripped_name [dict get $item_ref_info stripped_name] \
        -label $label \
        -parent_id [dict get $item_ref_info parent_id] \
        -item_id [dict get $item_ref_info item_id] \
        -package_id $package_id \
        -is_self_link $is_self_link

    # in case, we can't link, flush the href
    if {[:can_link [dict get $item_ref_info item_id]] == 0} {
      :references refused [dict get $item_ref_info item_id]
      if {[[self]::link exists href]} {
        [self]::link unset href
      }
    }

    [self]::link configure {*}$options
    set result [[self]::link]

    return $result
  }

  Page instproc new_link {
    -object_type
    -name
    -title
    -nls_language
    -return_url
    -parent_id
    page_package_id
  } {
    if {[info exists parent_id] && $parent_id eq ""} {
      unset parent_id
    }
    return [::$page_package_id make_link $page_package_id \
                edit-new object_type name title nls_language return_url parent_id autoname]
  }

  FormPage instproc new_link {
    -object_type
    -name
    -title
    -nls_language
    -parent_id
    -return_url
    page_package_id
  } {
    if {[info exists object_type]} {
      next
    } else {
      set template_id ${:page_template}
      if {![info exists parent_id]} {
        set parent_id [::$page_package_id folder_id]
      }
      set form [::$page_package_id pretty_link -parent_id $parent_id [::$template_id name]]
      return [::$page_package_id make_link -link $form $template_id \
                  create-new return_url name title nls_language]
    }
  }

  #
  # Handling page references. Use like e.g.
  #
  #   $page references clear
  #   $page references resolved [list $item_id [:type]]
  #   $page references get resolved
  #   $page references all
  #
  Page instproc references {submethod args} {
    #ns_log notice "---- ${:name} references $submethod $args"
    switch -- $submethod {
      clear {
        set :__references { unresolved {} resolved {} refused {} }
      }
      unresolved -
      refused -
      resolved {
        return [dict lappend :__references $submethod [lindex $args 0]]
      }
      get { return [dict get ${:__references} [lindex $args 0]] }
      all { return ${:__references} }
      default {error "unknown submethod: $submethod"}
    }
  }

  Page instproc anchor_parent_id {} {
    #
    # This method returns the parent_id used for rendering links
    # between double square brackets [[...]]. It can be overloaded for
    # more complex embedding situations
    #
    return ${:item_id}
  }

  Page instproc anchor {arg} {
    ad_try {
      set l [:create_link $arg]
    } on error {errorMsg} {
      return "<div class='errorMsg'>Error during processing of anchor ${arg}:<blockquote>$errorMsg</blockquote></div>"
    }
    if {$l eq ""} {
      return ""
    }

    if {[info exists :__RESOLVE_LOCAL] && [$l exists is_self_link] && [$l is_self_link]} {
      :set_resolve_context -package_id [:physical_package_id] -parent_id [:physical_parent_id]
      $l parent_id [:anchor_parent_id]
      set html [$l render]
      :reset_resolve_context
    } else {
      set html [$l render]
    }

    if {[info commands $l] ne ""} {
      $l destroy
    } else {
      ns_log notice "link object already destroyed. This might be due to a recursive inclusion"
    }
    return $html
  }


  Page instproc substitute_markup {{-context_obj ""} content} {
    if {${:mime_type} eq "text/enhanced"} {
      set content [ad_enhanced_text_to_html $content]
    }
    if {!${:do_substitutions}} {
      return $content
    }
    #
    # The provided content and the returned result are strings
    # containing HTML (unless we have other rich-text encodings).
    #
    # First get the potentially class specific regular expression
    # definitions.
    #
    set baseclass [expr {[[:info class] exists RE] ? [:info class] : [self class]}]
    $baseclass instvar RE markupmap
    #:log "-- baseclass for RE = $baseclass"

    #
    # Secondly, iterate line-wise over the text.
    #
    set output ""
    set l ""

    ad_try {
      if {$context_obj ne ""} {
        :set_resolve_context \
            -package_id [$context_obj package_id] -parent_id [$context_obj item_id]
        set :__ignore_self_in_links 1
      }

      foreach l0 [split $content \n] {
        append l [string map $markupmap(escape) $l0]
        if {[string first \{\{ $l] > -1 && [string first \}\} $l] == -1} {append l " "continue}
        set l [:regsub_eval $RE(anchor)  $l {:anchor  "\1""1"]
        set l [:regsub_eval $RE(div)     $l {:div     "\1"}]
        set l [:regsub_eval $RE(include) $l {:include_content "\1" "\2"}]
        #regsub -all -- $RE(clean) $l {\1} l
        regsub -all -- $RE(clean2) $l { \1} l
        set l [string map $markupmap(unescape) $l]
        append output $l \n
        set l ""
      }
    } on error {errorMsg} {
      error $errorMsg
    } finally {
      if {$context_obj ne ""} {
        unset :__ignore_self_in_links
        :reset_resolve_context
      }
    }
    #:log "--substitute_markup returns $output"
    return $output
  }


  Page instproc adp_subst {content} {
    #
    # The provided content and the returned result are strings
    # containing HTML.
    #
    #:msg "--adp_subst in ${:name} vars=[:info vars]"
    foreach __v [:info vars] {
      if {[info exists $__v]} continue
      #ns_log notice "import instvar $__v into current scope"
      :instvar $__v
    }
    #
    # The following block imports variables from the class to the
    # instance. Not sure, why we want this. TODO: Comment or remove.
    #
    #set __ignorelist {RE __defaults name_method object_type_key db_slot}
    #set __my_class [:info class]
    #foreach __v [$__my_class info vars] {
    #  if {[info exists $__v] || $__v in $__ignorelist} continue
    #  ns_log notice "import from $__my_class var $__v into [self]"
    #  $__my_class instvar $__v
    #}
    set __ignorelist {
      __v __vars __l __ignorelist __varlist __references __my_class
      __last_includelet text item_id content lang_links
    }

    # set variables current_* to ease personalization
    set current_user [::xo::cc set untrusted_user_id]
    set current_url [::xo::cc url]

    set __vars [info vars]
    regsub -all -- [template::adp_variable_regexp$content {\1@\2;noquote@} content_noquote
    #:log "--adp before adp_eval '[template::adp_level]'"

    set __l [string length $content]
    try {
      set __bufsize [ns_adp_ctl bufsize]
    } on error {errorMsg} {
      #
      # The adp buffer has limited size. For large pages, it might happen
      # that the buffer overflows. In AOLserver 4.5, we can increase the
      # buffer size. In 4.0.10, we are out of luck.
      #
      set __bufsize 0
    }
    if {$__bufsize > 0 && $__l > $__bufsize} {
      #
      # We have AOLserver 4.5 or NaviServer , we can increase the
      # bufsize
      #
      ns_adp_ctl bufsize [expr {$__l + 1024}]
    }
    set template_code [template::adp_compile -string $content_noquote]
    set my_parse_level [template::adp_level]
    ad_try {
      set template_value [template::adp_eval template_code]
    } on error {__errMsg} {
      #
      # Something went wrong during substitution; prepare a
      # user-friendly error message containing a listing of the
      # available variables.
      #
      # compute list of possible variables
      set __varlist [list]
      set __template_variables__ "<ul>\n"
      foreach __v [lsort $__vars] {
        if {[array exists $__v]} continue ;# don't report  arrays
        if {$__v in $__ignorelist} continue
        lappend __varlist $__v
        append __template_variables__ "<li><b>$__v:</b> '[set $__v]'\n"
      }
      append __template_variables__ "</ul>\n"
      set ::template::parse_level $my_parse_level
      #:log "--adp after adp_eval '[template::adp_level]' mpl=$my_parse_level"
      set template_value "<div class='errorMsg'>Error in Page $name: $__errMsg</div>$content<p>Possible values are$__template_variables__"
    }
    return $template_value
  }

  Page instproc get_description {-nr_chars content} {
    set revision_id ${:revision_id}
    set description ${:description}
    if {$description eq "" && $content ne ""} {
      set description [ad_html_text_convert -from text/html -to text/plain -- $content]
    }
    if {$description eq "" && $revision_id > 0} {
      set body [::xo::dc get_value -prepare integer get_description_from_syndication \
                    "select body from syndication where object_id = :revision_id" \
                    -default ""]
      set description [ad_html_text_convert -from text/html -to text/plain -- $body]
    }
    if {[info exists nr_chars] && [string length $description] > $nr_chars} {
      set description [string range $description 0 $nr_chars]...
    }
    return $description
  }

  Page instproc render_content {} {
    #:log "-- '${:text}'"
    lassign ${:text} html mime
    if {[:render_adp]} {
      set html [:adp_subst $html]
    }
    return [:substitute_markup $html]
  }

  Page instproc set_content {text} {
    :text [list [string map [list >> "\n&gt;&gt;" << "&lt;&lt;\n"] \
                     [string trim $text " \n"]] text/html]
  }

  Page instproc get_rich_text_spec {field_name default} {
    set package_id ${:package_id}
    set spec ""
    #:msg WidgetSpecs=[::$package_id get_parameter -check_query_parameter false WidgetSpecs]
    foreach {s widget_spec} [::$package_id get_parameter -check_query_parameter false WidgetSpecs] {
      lassign [split $s ,] page_name var_name
      # in case we have no name (edit new page) we use the first value or the default.
      set name [expr {[info exists :name] ? ${:name} : $page_name}]
      #:msg "--w T.name = '$name' var=$page_name ([string match $page_name $name]), $var_name $field_name ([string match $var_name $field_name])"
      if {[string match $page_name $name] &&
          [string match $var_name $field_name]} {
        set spec $widget_spec
        #:msg "setting spec to $spec"
        break
      }
    }
    if {$spec eq ""} {
      return $default
    }
    return $field_name:$spec
  }

  Page instproc validate=name {name} {
    #:log "---- validate=name $name is called"
    upvar nls_language nls_language
    set success [::xowiki::validate_name [self]]
    if {$success} {
      set actual_length [string length $name]
      set max_length 400
      if {$actual_length > $max_length} {
        set errorMsg [_ acs-tcl.lt_name_is_too_long__Ple \
                          [list name $name max_length $max_length actual_length $actual_length]]
        set success 0
      }
    } else {
      #
      # The plain form validation has signaled, that the name is not
      # ok. Try to provide a more detailed error message.
      #
      if {![:istype ::xowiki::File] && [regexp {^[a-zA-Z][a-zA-Z]:$} $name]} {
        set errorMsg [_ xowiki.Page-validate_name-invalid_name [list value $name]]
      } else {
        set errorMsg [_ xowiki.Page-validate_name-duplicate_item [list value $name]]
      }
    }

    if {$success} {
      #
      # Set the instance variable with a potentially prefixed
      # name. The classical validators (like xowiki::validate_name) do
      # just an upvar. Therefore, the "name" value is already
      # normalized and prefixed.
      #
      set :name $name
    } else {
      uplevel [list set errorMsg $errorMsg]
    }
    return $success
  }

  Page instproc validate=page_order {value} {
    if {[info exists :page_order]} {
      set page_order [string trim $value " ."]
      set :page_order $page_order
      # :log "validate=page_order '$value' -> '$page_order'"
      return [expr {![regexp {[^0-9a-zA-Z_.]} $page_order]}]
    }
    return 1
  }

  Page instproc references_update {resolved {unresolved {}}} {
    #:log "references_update resolved '$resolved' unresolved '$unresolved'"
    set item_id ${:item_id}
    ::xo::dc dml -prepare integer delete_references \
        "delete from xowiki_references where page = :item_id"
    ::xo::dc dml -prepare integer delete_unresolved_references \
        "delete from xowiki_unresolved_references where page = :item_id"
    foreach ref $resolved {
      lassign $ref r link_type
      ::xo::dc dml insert_reference \
          "insert into xowiki_references (reference, link_type, page) \
           values (:r,:link_type,:item_id)"
    }
    foreach ref $unresolved {
      dict with ref {
        ::xo::dc dml insert_unresolved_reference \
            "insert into xowiki_unresolved_references (page, parent_id, name, link_type) \
            values (:item_id,:parent_id,:name,:link_type)"
      }
    }
  }

  Page instproc check_unresolved_references {} {
    #:log "check_unresolved_references: name ${:name} parent_id ${:parent_id}"
    set parent_id ${:parent_id}
    set name ${:name}
    foreach i [xo::dc list -prepare integer,text items_with_unresolved_references {
      SELECT page from xowiki_unresolved_references
      WHERE  parent_id = :parent_id
      AND    name = :name
    }] {
      set page [::xo::db::CrClass get_instance_from_db -item_id $i]
      #:log "==== check_unresolved_references found page [$page name] with a broken reference to the new page ${:name}"
      $page render -update_references all -with_footer false
    }
  }

  Page proc container_already_rendered {field} {
    if {![info exists ::xowiki_page_item_id_rendered]} {
      return ""
    }
    #:log "--OMIT and not $field in ([ns_dbquotelist $::xowiki_page_item_id_rendered])"
    return "and not $field in ([ns_dbquotelist $::xowiki_page_item_id_rendered])"
  }

  Page instproc htmlFooter {{-content ""}} {
    if {[info exists :__no_footer]} {return ""}
    set package_id ${:package_id}
    set footer ""

    if {[ns_conn isconnected]} {
      set url         "[ns_conn location][::xo::cc url]"
      set package_url "[ns_conn location][::$package_id package_url]"
    }

    set tags ""
    if {[::$package_id get_parameter with_tags:boolean 1] &&
        ![:exists_query_parameter no_tags] &&
        [::xo::cc user_id] != 0
      } {
      set tag_content [:include my-tags]
      set tag_includelet ${:__last_includelet}
      if {[$tag_includelet exists tags]} {
        set tags [$tag_includelet set tags]
      }
    } else {
      set tag_content ""
    }

    if {[::$package_id get_parameter with_digg:boolean 0] && [info exists url]} {
      if {![info exists description]} {set description [:get_description $content]}
      append footer "<div style='float: right'>" \
          [:include [list digg -description $description -url $url]] "</div>\n"
    }

    if {[::$package_id get_parameter with_delicious:boolean 0] && [info exists url]} {
      if {![info exists description]} {set description [:get_description $content]}
      append footer "<div style='float: right; padding-right: 10px;'>" \
          [:include [list delicious -description $description -url $url -tags $tags]] \
          "</div>\n"
    }

    if {[::$package_id get_parameter with_yahoo_publisher:boolean 0] && [info exists package_url]} {
      set publisher [::$package_id get_parameter "my_yahoo_publisher" \
                         [::xo::get_user_name [::xo::cc user_id]]]
      append footer \
          "<div style='float: right; padding-right: 10px;'>" \
          [:include [list my-yahoo-publisher \
                         -publisher $publisher \
                         -rssurl "$package_url?rss"]] \
          "</div>\n"
    }

    if {[::$package_id get_parameter show_page_references:boolean 1]} {
      append footer [:include my-references]
    }

    if {[::$package_id get_parameter show_per_object_categories:boolean 1]} {
      set html [:include my-categories]
      if {$html ne ""} {
        append footer $html <br>
      }
      set categories_includelet ${:__last_includelet}
    }

    append footer $tag_content

    if {[::$package_id get_parameter with_general_comments:boolean 0] &&
        ![:exists_query_parameter no_gc]} {
      append footer [:include my-general-comments]
    }

    if {$footer ne ""} {
      # make sure, the
      append footer "<div class='visual-clear'><!-- --></div>"
    }

    return  "<div class='item-footer'>$footer</div>\n"
  }


  Page instproc footer {} {
    return ""
  }

  Page instproc get_content {} {
    return [:render -with_footer false -update_references never]
  }

  Page ad_instproc render {
    {-update_references unresolved}
    {-with_footer:boolean true}
  } {

    Render a wiki page with some optional features, such as including
    a footer or updating references for this page.

    @param update_references might be "all""unresolved" or "never"
    @param with_footer boolean value
    @return rendered HTML content.
  } {
    #
    # prepare language links
    #
    array set :lang_links {found "" undefined ""}
    #
    # prepare references management
    #
    :references clear
    if {[info exists :__extra_references]} {
      #
      # xowiki content-flow uses extra references, e.g. to forms.
      # TODO: provide a better interface for providing these kind of
      # non-link references.
      #
      foreach ref ${:__extra_references} {
        :references resolved $ref
      }
      unset :__extra_references
    }
    #
    # Get page content and care about reference management.
    #
    set content [:render_content]
    #
    # Clear old reference and record new ones in cases updating
    # references is activated "always" or just for unresolved
    # references.
    #
    set unresolved_references [:references get unresolved]

    if {$update_references eq "all"
        || ($update_references eq "unresolved" && [llength $unresolved_references] > 0)
      } {
      :references_update \
          [lsort -unique [:references get resolved]] \
          [lsort -unique $unresolved_references]
    }
    #
    #:log "Page ${:name} render with_footer $with_footer - [ns_conn isconnected] - [catch {ns_conn content}]"
    #
    # handle footer
    #
    if {$with_footer && [::xo::cc get_parameter content-type:graph text/html] eq "text/html"} {
      append content "<DIV class='content-chunk-footer'>"
      if {![info exists :__no_footer] && ![::xo::cc get_parameter __no_footer:boolean 0]} {
        append content [:footer]
      }
      append content "</DIV>\n"
    }
    return $content
  }

  #
  # The method "search_render" is called by the search indexer via
  # ::xowiki::datasource and returns HTML and the keywords for the
  # search. By defining this as a method, it is possible to define a
  # different indexer e.g. via subclassing or for each workflow. The
  # method returns a list of attribute value pairs containing "html"
  # and keywords".  Below is an example of a workflow specific search
  # content.
  #
  #   ${:object} proc search_render {} {
  #        return [list html "Hello World" keywords "hello world"]
  #   }
  #
  #
  Page instproc search_render {} {
    set :__no_form_page_footer 1
    set html [:render -update_references none]
    unset :__no_form_page_footer

    foreach tag {h1 h2 h3 h4 h5 b strong} {
      foreach {match words} [regexp -all -inline "<$tag>(\[^<\]+)</$tag>" $html] {
        foreach w [split $words] {
          if {$w eq ""} continue
          set word($w) 1
        }
      }
    }
    foreach tag [::xowiki::Page get_tags -package_id ${:package_id} -item_id ${:item_id}] {
      set word($tag) 1
    }
    #:log [list html $html keywords [array names word]]
    return [list mime text/html html $html keywords [array names word] text ""]
  }

  #
  # The method "notification_render" is called by the notification
  # procs.  By re-defining this method (e.g. in a workflow), it is
  # possible to produce a different notification text.
  # The method returns an HTML text.
  #
  Page instproc notification_render {} {
    return [:render]
  }

  FormPage instproc notification_render {} {
    if {[:is_link_page] || [:is_folder_page]} {
      return ""
    } else {
      return [next]
    }
  }

  #
  # The method "notification_notify" calls typically the notification
  # updater on the current page. It might be used as well to trigger
  # notifications on other pages (in other maybe packages), when the
  # page content is e.g. linked.
  #
  Page instproc notification_notify {} {
    ::xowiki::notification::do_notifications -page [self]
  }

  #
  # The method "notification_detail_link" is called from
  # do_notifications to provide a link back to the context, where the
  # new/modified item can be viewed in detail. It has to return an
  # html and a text component.
  #
  Page instproc notification_detail_link {} {
    set link [:pretty_link -absolute 1]
    append html "<p>For more details, see <a href='[ns_quotehtml $link]'>[ns_quotehtml ${:title}]</a></p>"
    append text "\nFor more details, see $link ...\n"
    return [list html $html text $text]
  }

  #
  # The method "notification_subject" is called from
  # do_notifications to provide the subject line for notifications.
  # The "-category_label" might be empty.
  #
  Page instproc notification_subject {-instance_name {-category_label ""} -state} {
    if {$category_label eq ""} {
      return "\[$instance_name\]: ${:title} ($state)"
    } else {
      return "\[$instance_name\] $category_label: ${:title} ($state)"
    }
  }

  #
  # Update xowiki_last_visited table
  #
  Page instproc record_last_visited {-user_id} {
    set item_id ${:item_id}
    set package_id ${:package_id}
    if {![info exists user_id]} {set user_id [::xo::cc set untrusted_user_id]}
    if {$user_id > 0} {
      # only record information for authenticated users
      ::xo::dc transaction {
        set rows [xo::dc dml -prepare integer,integer update_last_visisted {
          update xowiki_last_visited set time = CURRENT_TIMESTAMP, count = count + 1
          where page_id = :item_id and user_id = :user_id
        }]
        if {$rows ne "" && $rows < 1} {
          ::xo::dc dml insert_last_visisted \
              "insert into xowiki_last_visited (page_id, package_id, user_id, count, time) \
               values (:item_id, :package_id, :user_id, 1, CURRENT_TIMESTAMP)"
        }
      }
    }
  }

  #
  # Some utility functions, called on different kind of pages
  #
  Page instproc get_html_from_content {content} {
    # Check, whether we got the content through a classic 2-element
    # OpenACS templating widget or directly.  If the list is not
    # well-formed, it must be contained directly.
    if {![catch {set l [llength $content]}]
        && $l == 2
        && [string match "text/*" [lindex $content 1]]} {
      return [lindex $content 0]
    }
    return $content
  }

  Page instproc content_header_append {text} {
    #
    # This function is to be called on pages that want to prepend
    # content prior to the main content. This is especially important
    # for HTML forms (e.g. produced by the xowiki::Form renderer),
    # where the form-body is not allowed to contain nested forms.
    #
    append ::__xowiki__content_header $text \n
  }

  Page instproc content_header_get {} {
    if {[info exists ::__xowiki__content_header]} {
      return $::__xowiki__content_header
    }
  }


  Page instproc form_field_index {form_field_objs} {
    set marker ::__computed_form_field_names($form_field_objs)
    if {[info exists $marker]} return

    foreach form_field_obj $form_field_objs {
      if {![$form_field_obj istype ::xowiki::formfield::FormField]} continue
      set ::_form_field_names([$form_field_obj name]) $form_field_obj
      :form_field_index [$form_field_obj info children]
    }
    set $marker 1
  }

  Page instproc form_field_flush_cache {} {
    #
    # Flush all cached form_field_names.
    #
    unset -nocomplain ::_form_field_names
  }

  Page instproc form_field_exists {name} {
    return [info exists ::_form_field_names($name)]
  }

  Page instproc __debug_known_field_names {msg} {
    set fields {}
    foreach name [lsort [array names ::_form_field_names]] {
      set f $::_form_field_names($name)
      append fields "  $name\t[$f info class]\t [$f spec]\n"
    }
    append fields "Repeat container:\n"
    foreach f [::xowiki::formfield::repeatContainer info instances] {
      append fields "$f\t[$f name]\t [$f spec]\n"
      foreach component [$f components] {
        append fields "... [$component name]\t[$component info class]\t [$component spec]\n"
        if {[$component istype ::xowiki::formfield::CompoundField]} {
          foreach c [$component components] {
            append fields "..... [$c name]\t[$c info class]\t [$c spec]\n"
          }
        }
      }
    }
    #ns_log notice "dynamic repeat field $msg: fields & specs:\n$fields"
  }

  Page instproc lookup_form_field {
    -name:required
    form_fields
  } {
    :form_field_index $form_fields
    #ns_log notice "lookup_form_field <$name>"

    set key ::_form_field_names($name)
    if {[info exists $key]} {
      return [set $key]
    }
    #
    # We have here a non-existing form-field. Maybe the entry in the
    # form was dynamically created, so we create it here on the fly...
    #
    # For forms with variable numbers of entries, we allow wild-cards
    # in the field-names of the form constraints.
    #
    foreach name_and_spec [:get_form_constraints] {
      regexp {^([^:]+):(.*)$} $name_and_spec _ spec_name short_spec

      if {[string match $spec_name $name]} {
        set f [:create_form_fields_from_form_constraints [list $name:$short_spec]]
        set $key $f
        return $f
      }
    }

    #
    # Maybe, this was a repeat field, and we have to create the nth
    # component dynamically.
    #
    set components [split $name .]
    set path [lindex $components 0]
    #ns_log notice "dynamic repeat field name '$name' -> components <$components>"

    foreach c [lrange $components 1 end] {
      if {[string is integer -strict $c]} {
        # this looks like a repeat component
        #ns_log notice "dynamic repeat field root <$path> number $c exists? [info exists ::_form_field_names($path)]"

        if {[info exists ::_form_field_names($path)]} {
          #
          # The root field exists, so add the component.
          #
          set repeatField [set ::_form_field_names($path)]
          #
          # Add all components from 1 to specified number to the list,
          # unless restricted by the max value. This frees us from
          # potential problems, when the browser sends the form fields
          # in an unexpected order. The resulting components will be
          # always in the numbered order.
          #
          set max [$repeatField max]
          if {$max > $c} {
            set max $c
          }
          for {set i 1} {$i <= $max} {incr i} {
            if {![info exists ::_form_field_names($path.$i)]} {
              set f [$repeatField require_component $i]
              #ns_log notice "dynamic repeat field created $path.$i -> $f"
              :form_field_index $f
            }
          }
        } else {
          :__debug_known_field_names "<$path> needed to create <$path.$c>"
        }
      }
      append path . $c
    }
    #
    # We might have created in the loop above the required
    # formfield. If so, return it.
    #
    if {[info exists $key]} {
      #ns_log notice "dynamic repeat 2nd lookup for $key succeeds"
      return [set $key]
    }

    if {$name ni {langmarks fontname fontsize formatblock} && ![string match *__locale $name]} {
      set names [list]
      #xo::show_stack
      foreach f $form_fields {lappend names [$f name]}
      :msg "No form field with name '$name' found\
            (available fields: [lsort [array names ::_form_field_names]])"
      ns_log warning "====== lookup_form_field: No form field with name '$name' found" \
          "(available fields: [lsort [array names ::_form_field_names]])"
    }
    set f [:create_form_fields_from_form_constraints [list $name:text]]
    set $key $f
    return $f
  }

  Page instproc lookup_cached_form_field {
    -name:required
  } {
    set key ::_form_field_names($name)
    #:msg "FOUND($name)=[info exists $key]"
    if {[info exists $key]} {
      return [set $key]
    }
    error "No form field with name $name found"
  }

  Page instproc show_fields {form_fields {msg ""}} {
    # this method is for debugging only
    foreach f $form_fields { append msg "[$f name] [namespace tail [$f info class]], " }
    :msg $msg
    :log "form_fields: $msg"
  }



  Page instproc translate {-from -to text} {
    set langpair $from|$to
    set url http://translate.google.com/#$from/$to/$text
    set request [util::http::get -url $url]
    set status [dict get $request status]
    set data [expr {[dict exists $request page] ? [dict get $request page] : ""}]

    #:msg status=[$r set status]
    if {$status == 200} {
      #:msg data=$data
      dom parse -html -- $data doc
      $doc documentElement root
      set n [$root selectNodes {//*[@id="result_box"]}]
      :msg "$text $from=>$to node '$n'"
      if {$n ne ""} {return [$n asText]}
    }
    util_user_message -message "Could not translate text, \
        status=$status"
    return "untranslated: $text"
  }


  Page instproc create_form_page_instance {
    -name:required
    -package_id
    -parent_id
    {-text ""}
    {-instance_attributes ""}
    {-default_variables ""}
    {-nls_language ""}
    {-state initial}
    {-creation_user ""}
    {-publish_status production}
    {-source_item_id ""}
  } {
    set ia [dict merge [:default_instance_attributes] $instance_attributes]

    if {$nls_language eq ""} {
      set nls_language [:query_parameter nls_language:wordchar [:nls_language]]
    }
    #
    # Take the value of the instance variables package_id and
    # parent_id as default.
    #
    if {![info exists package_id]} {
      set package_id ${:package_id}
    }
    if {![info exists parent_id]}  {
      set parent_id ${:parent_id}
    }

    if {$creation_user eq ""} {
      #
      # When no creation_user is provided, take the current user_id,
      # but take care as well for situations, where no connections is
      # available.
      #
      set context [::$package_id context]
      if {![nsf::is object $context]} {
        ::xo::ConnectionContext require \
            -package_id $package_id \
            -url [::$package_id pretty_link ${:name}]
      }
      set creation_user [$context user_id]
    }
    set f [FormPage new -destroy_on_cleanup \
               -name $name \
               -text $text \
               -package_id $package_id \
               -parent_id $parent_id \
               -nls_language $nls_language \
               -publish_status $publish_status \
               -creation_user $creation_user \
               -state $state \
               -instance_attributes $ia \
               -page_template ${:item_id}]

    # Make sure to load the instance attributes
    #$f array set __ia [$f instance_attributes]

    #
    # Call the application specific initialization, when a FormPage is
    # initially created. This is used to control the life-cycle of
    # FormPages.
    #
    $f initialize

    #
    # If we copy an item, we use source_item_id to provide defaults.
    #
    if {$source_item_id ne ""} {
      set sourceObj [FormPage get_instance_from_db -item_id $source_item_id]
      $f copy_content_vars -from_object $sourceObj -except name

      #
      # In case, we want manual autonaming, the following could be
      # used.
      #
      #set name "[::xowiki::autoname new -parent_id $source_item_id -name ${:name}]"
      #::$package_id get_lang_and_name -name $name lang name
      #$f set name $name
      #ns_log notice "FINAL NAME <$name>"
      #:msg nls=[$f nls_language],source-nls=[$source nls_language]
    }
    foreach {att value} $default_variables {
      $f set $att $value
    }

    #
    # Finally provide base for auto-titles.
    #
    $f set __title_prefix ${:title}

    return $f
  }


  #
  # Methods of ::xowiki::PlainPage
  #

  PlainPage parameter {
    {render_adp 0}
  }
  PlainPage array set RE {
    include {{{(.+?)}}([^\}]|$)}
    anchor  {\\\[\\\[([^\]]+?)\\\]\\\]}
    div     {>>([^<]*?)<<}
    clean   {[\\](\{\{|>>|\[\[)}
    clean2  {(--DUMMY NOT USED--)}
  }
  PlainPage set markupmap(escape)   [list "\\\[\["  \03\01  "\\\{\{"  \03\02   {\>>}  \03\03]
  PlainPage set markupmap(unescape) [list  \03\01 "\[\["     \03\02 "\{\{"      \03\03 {>>}]

  PlainPage instproc unescape string {
    return $string
  }

  PlainPage instproc render_content {} {
    set html ${:text}
    if {[:render_adp]} {
      set html [:adp_subst $html]
    }
    return [:substitute_markup $html]
  }
  PlainPage instproc set_content {text} {
    set :text $text
  }

  PlainPage instproc substitute_markup {{-context_obj ""} raw_content} {

    #
    # The provided text is a raw text that is transformed into HTML
    # markup for links etc.
    #
    [self class] instvar RE markupmap
    if {!${:do_substitutions}} {
      return $raw_content
    }
    set html ""
    try {
      if {$context_obj ne ""} {
        :set_resolve_context \
            -package_id [$context_obj package_id] -parent_id [$context_obj item_id]
      }
      foreach l [split $raw_content \n] {
        set l [string map $markupmap(escape) $l]
        set l [:regsub_eval $RE(anchor)  $l {:anchor  "\1"}]
        set l [:regsub_eval $RE(div)     $l {:div     "\1"}]
        set l [:regsub_eval $RE(include) $l {:include_content "\1" ""}]
        #regsub -all -- $RE(clean) $l {\1} l
        set l [string map $markupmap(unescape) $l]
        append html $l \n
      }
    } on error {errorMsg} {
      error $errorMsg
    } finally {
      if {$context_obj ne ""} {
        :reset_resolve_context
      }
    }
    return $html
  }

  #
  # Methods of ::xowiki::File
  #

  File parameter {
    {render_adp 0}
  }
  File instproc build_name {name {fn ""}} {
    if {$name ne ""} {
      set stripped_name $name
      regexp {^(.*):(.*)$} $name _ _t stripped_name
    } else {
      set stripped_name $fn
      # Internet explorer seems to transmit the full path of the
      # filename. Just use the last part in such cases as name.
      regexp {[/\\]([^/\\]+)$} $stripped_name _ stripped_name
    }
    return file:[::${:package_id} normalize_name $stripped_name]
  }
  File instproc full_file_name {} {
    if {![info exists :full_file_name]} {
      if {[info exists :revision_id]} {
        #
        # For a given revision_id, the full_file_name will never
        # change.  Therefore, we can easily cache the full filename
        # for the revision_id.
        #
        set :full_file_name [::xowiki::cache eval -partition_key ${:revision_id} ffn-${:revision_id} {
          return [content::revision::get_cr_file_path -revision_id ${:revision_id}]
        }]
        #:log "--F setting full-file-name of ${:revision_id}  ${:full_file_name}"
      }
    }
    return ${:full_file_name}
  }

  File instproc search_render {} {
    #  array set "" {mime text/html text "" html "" keywords ""}
    if {${:mime_type} eq "text/plain"} {
      set f [open [:full_file_name] r]; set data [read $f]; close $f
      set result [list text $data mime text/plain]
    } elseif {[::namespace which ::search::convert::binary_to_text] ne ""} {
      set txt [search::convert::binary_to_text \
                   -filename [:full_file_name] \
                   -mime_type ${:mime_type}]
      set result [list text $txt mime text/plain]
    } else {
      set result [list text "" mime text/plain]
    }

    #ns_log notice "search_render returns $result"
    return $result
  }

  File instproc html_content {{-add_sections_to_folder_tree 0} -owner} {
    set parent_id ${:parent_id}
    set fileName [:full_file_name]

    set f [open $fileName r]; set data [read $f]; close $f

    # Ugly hack to fight against a problem with tDom: asHTML strips
    # spaces between a </span> and the following <span>"
    #regsub -all -- "/span>      <span" $data "/span>\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;<span" data
    #regsub -all -- "/span>     <span" $data "/span>\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;<span" data
    #regsub -all -- "/span>    <span" $data "/span>\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;<span" data
    #regsub -all -- "/span>   <span" $data "/span>\\&nbsp;\\&nbsp;\\&nbsp;<span" data
    #regsub -all -- "/span>  <span" $data "/span>\\&nbsp;\\&nbsp;<span" data

    regsub -all -- "/span> " $data "/span>\\&nbsp;" data
    regsub -all -- " <span " $data "\\&nbsp;<span " data
    regsub -all -- "/span>\n<span " $data "/span><br><span " data
    regsub -all -- "/span>\n\n<span " $data "/span><br><br><span " data

    dom parse -simple -- $data doc
    $doc documentElement root

    #
    # substitute relative links to download links in the same folder
    #
    set prefix [::$parent_id pretty_link -absolute true -download true]
    foreach n [$root selectNodes //img] {
      set src [$n getAttribute src]
      if {[regexp {^[^/]} $src]} {
        $n setAttribute src $prefix/$src
        #:msg "setting src to $prefix/$src"
      }
    }

    #
    # In case, the switch is activated, and we have a menubar, add the
    # top level section
    #
    if {$add_sections_to_folder_tree && [nsf::is object ::__xowiki__MenuBar]} {
      $owner set book_mode 1
      set pages [::xo::OrderedComposite new -destroy_on_cleanup]
      if {$add_sections_to_folder_tree == 1} {
        set selector //h2
      } else {
        set selector {//h2 | //h3}
      }

      set order 0
      foreach n [$root selectNodes $selector] {
        if {[$n hasAttribute id]} {
          set name [$n getAttribute id]
        } else {
          set name "section $n"
        }
        set o [::xotcl::Object new]
        $o set page_order [incr $order]
        $o set title [$n asText]

        set e [$doc createElement a]
        $e setAttribute name $name
        [$n parentNode] insertBefore $e $n

        $o set name $name
        $pages add $o
      }

      #$o instvar page_order title name

      ::__xowiki__MenuBar additional_sub_menu -kind folder -pages $pages -owner $owner
    }

    #
    # return content of body
    #
    set content ""
    foreach n [$root selectNodes //body/*] { append content [$n asHTML] \n }

    return $content
  }

  File instproc render_content {} {
    set parent_id ${:parent_id}
    set package_id ${:package_id}
    # don't require permissions here, such that rss can present the link
    #set page_link [::$package_id make_link -privilege public [self] download ""]

    set ctx [::$package_id context]
    set revision_id [$ctx query_parameter revision_id:integer]
    set query [expr {$revision_id ne "" ? "revision_id=$revision_id" : ""}]
    set page_link [:pretty_link -download true -query $query]
    if {[$ctx query_parameter html-content] ne ""} {
      return [:html_content]
    }

    #:log "--F page_link=$page_link ---- "
    set t [TableWidget new -volatile \
               -columns {
                 AnchorField name -label [_ xowiki.Page-name]
                 Field mime_type -label "#xowiki.content_type#"
                 Field last_modified -label "#xowiki.Page-last_modified#"
                 Field mod_user -label "#xowiki.By_user#"
                 Field size -label "#xowiki.Size# (Bytes)"
               }]

    regsub {[.][0-9]+([^0-9])} ${:last_modified} {\1} last_modified
    ::$package_id get_lang_and_name -name ${:name} lang stripped_name
    set label $stripped_name

    $t add \
        -name $stripped_name \
        -mime_type ${:mime_type} \
        -name.href $page_link \
        -last_modified $last_modified \
        -mod_user [::xo::get_user_name ${:creation_user}] \
        -size [file size [:full_file_name]]

    switch -glob ${:mime_type} {
      image/* {
        set l [Link new \
                   -page [self] -query $query \
                   -type image -name ${:name} -lang "" \
                   -stripped_name $stripped_name -label $label \
                   -parent_id $parent_id -item_id ${:item_id} -package_id $package_id]
        set preview "<div >[$l render]</div>"
        $l destroy
      }
      text/plain {
        set text [::xo::read_file [:full_file_name]]
        set preview "<pre class='code'>[::xowiki::Includelet html_encode $text]</pre>"
      }
      default {set preview ""}
    }
    return "$preview[$t asHTML]\n<p>${:description}</p>"
  }

  PodcastItem instproc render_content {} {
    set content [next]
    append content <ul>
    foreach {label var} {
      #xowiki.title# title
      #xowiki.PodcastItem-subtitle# subtitle
      #xowiki.Page-creator# creator
      #xowiki.PodcastItem-pub_date# pub_date
      #xowiki.PodcastItem-duration# duration
      #xowiki.PodcastItem-keywords# keywords
    } {
      append content "<li><em>$label:</em> [set :$var]\n"
    }
    append content </ul>
    return $content
  }

  #
  # PageTemplate specifics
  #
  PageTemplate parameter {
    {render_adp 0}
  }
  PageTemplate instproc count_usages {
    {-package_id 0}
    {-parent_id 0}
    {-publish_status ready}
  } {
    return [::xowiki::PageTemplate count_usages -package_id $package_id -parent_id $parent_id \
                -item_id ${:item_id} -publish_status $publish_status]
  }

  PageTemplate proc count_usages {
                                  {-package_id:integer 0}
                                  {-parent_id:integer 0}
                                  -item_id:required
                                  {-publish_status ready}
                                } {
    set publish_status_clause [::xowiki::Includelet publish_status_clause -base_table i $publish_status]
    if {$package_id} {
      set bt "xowiki_page_instancei"
      set package_clause "and object_package_id = :package_id"
    } else {
      set bt "xowiki_page_instance"
      set package_clause ""
    }
    if {$parent_id} {
      set parent_id_clause "and parent_id = :parent_id"
    } else {
      set parent_id_clause ""
    }
    set count [::xo::dc get_value count_usages \
                   "select count(page_instance_id) from $bt, cr_items i  \
            where page_template = :item_id \
                        $publish_status_clause $package_clause $parent_id_clause \
                        and page_instance_id = coalesce(i.live_revision,i.latest_revision)"]
    return $count
  }

  Page instproc css_class_name {{-margin_form:boolean true}} {
    #
    # Determine the CSS class name for xowiki forms
    #
    set name ""
    if {$margin_form} {
      set css [::template::CSS class margin-form]
      if {$css ne ""} {
        set name "$css "
      }
    }
    return [append name [::xowiki::utility formCSSclass ${:name}]]
  }

  #
  # PageInstance methods
  #

  PageInstance proc get_list_from_form_constraints {-name -form_constraints} {
    set spec [::xowiki::PageInstance get_short_spec_from_form_constraints \
                  -name $name \
                  -form_constraints $form_constraints]
    set result [list]
    foreach spec [split $spec ,] {
      if {[regexp {^([^=]+)=(.*)$} $spec _ attr value]} {
        lappend result $attr $value
      } else {
        :log "can't parse $spec in attribute and value; ignoring"
      }
    }
    return $result
  }

  PageInstance proc get_short_spec_from_form_constraints {-name -form_constraints} {
    #
    # For the time being we cache the parsed form_constraints. Without
    # caching, the proc takes 87 microseconds, with chaching, it is
    # significantly faster.
    #
    # via ns_cache {6.153537846215379 microseconds per iteration}
    # via nsv {3.865795920407959 microseconds per iteration}
    #
    set varname ::__xo_[ns_md5 $form_constraints]
    set dict ""

    if {![nsv_get parsed_fcs $varname dict]} {
      #
      # Not parsed yet
      #
      foreach name_and_spec $form_constraints {
        set p [string first : $name_and_spec]
        if {$p > -1} {
          dict set dict \
              [string range $name_and_spec 0 $p-1] \
              [string range $name_and_spec $p+1 end]
        } else {
          ad_log warning "get_short_spec_from_form_constraints: name_and_spec <$name_and_spec> is invalid"
        }
      }
      nsv_set parsed_fcs $varname $dict
    }
    if {[dict exists $dict $name]} {
      return [dict get $dict $name]
    }
    return ""
  }
  #
  # Set the nsv array parsed_fcs to dummy values to avoid potential
  # exceptions on operations requiring its existence.
  #
  nsv_set parsed_fcs . .

  PageInstance instproc field_names_from_form_constraints {} {
    set form_constraints [:get_form_constraints]
    set result {}
    foreach name_and_spec $form_constraints {
      regexp {^([^:]+):} $name_and_spec _ name
      if {[string range $name 0 0] eq "@"} {
        # return no aggregated (pseudo) form field names
        continue
      }
      lappend result $name
    }
    return $result
  }

  PageInstance instproc get_short_spec {{-form_constraints ""} name} {
    #
    # In case, the form_constraints are provided, get the short-spec
    # from there, otherwise compute form_constraints via
    # method "get_form_constraints".
    #
    if {$form_constraints eq ""} {
      set form_constraints [:get_form_constraints]
    }
    if {$form_constraints ne ""} {
      set s [::xowiki::PageInstance get_short_spec_from_form_constraints \
                 -name $name -form_constraints $form_constraints]
      #:msg "get_short_spec $name c=$form_constraints => '$s'"
      return $s
    }
    return ""
  }

  PageInstance instproc get_field_label {name value} {
    set short_spec [:get_short_spec $name]
    if {$short_spec ne ""} {
      set f [::xowiki::formfield::FormField new -volatile -name $name -spec $short_spec]
      if {![$f exists show_raw_value]} {
        set value [$f field_value $value]
      }
    }
    return $value
  }

  PageInstance instproc widget_spec_from_folder_object {name given_template_name} {
    # get the widget field specifications from the payload of the folder object
    # for a field with a specified name in a specified page template
    foreach {s widget_spec} [::${:package_id} get_parameter -check_query_parameter false WidgetSpecs] {
      lassign [split $s ,] template_name var_name
      #ns_log notice "--w template_name $template_name, given '$given_template_name' varname=$var_name name=$name"
      if {([string match $template_name $given_template_name] || $given_template_name eq "") &&
          [string match $var_name $name]} {
        #ns_log notice "--w using $widget_spec for $name"
        return $widget_spec
      }
    }
    return ""
  }

  PageInstance instproc get_field_type {name default_spec} {
    #:log "--w"
    # get widget spec from folder (highest priority)
    set spec [:widget_spec_from_folder_object $name [${:page_template} set name]]
    if {$spec ne ""} {
      return $spec
    }
    # get widget spec from attribute definition
    set f [:create_raw_form_field -name $name -slot [:find_slot $name]]
    if {$f ne ""} {
      return [$f asWidgetSpec]
    }
    # use default widget spec
    return $default_spec
  }

  PageInstance instproc get_form {} {
    # get the (HTML) form of the ::xowiki::PageTemplates/::xowiki::Form
    return [:get_html_from_content [:get_from_template form]]
  }

  PageInstance instproc get_template_object {} {
    set id ${:page_template}
    if {![nsf::is object ::$id]} {
      ::xo::db::CrClass get_instance_from_db -item_id $id
    }
    return ::$id
  }

  PageInstance instproc get_form_constraints {{-trylocal false}} {
    # PageInstances have no form_constraints
    return ""
  }

  #FormPage instproc save args {
  #  :debug_msg [set :instance attributes]
  #  :log "IA=${:instance_attributes}"
  #  next
  #}

  FormPage instproc get_anon_instances {} {
    # maybe overloaded from WorkFlow
    :get_from_template anon_instances f
  }

  FormPage instproc get_form_constraints {{-trylocal false}} {
    #
    # This method os likely to be overloaded, maybe by xowf.
    #
    #:msg "is_form=[:is_form]"
    if {$trylocal && [:is_form]} {
      return [:property form_constraints]
    } else {
      #:msg "get_form_constraints returns '[:get_from_template form_constraints]'"
      return [:get_from_template form_constraints]
    }
  }

  FormPage instproc set_content {text} {
    if {$text eq ""} {
      set :text $text
    } else {
      next
    }
  }

  PageInstance ad_instproc get_from_template {var {default ""}} {
    Get a property from the parent object (template). The parent
    object might by either an ::xowiki::Form or an ::xowiki::FormPage

    @return either the property value or a default value
  } {
    set form_obj [:get_template_object]

    #:msg "get $var from template form_obj=$form_obj [$form_obj info class]"

    # The resulting page should be either a Form (PageTemplate) or
    # a FormPage (PageInstance)
    #
    #:msg "parent of self ${:name} is [$form_obj name] type [$form_obj info class]"
    #
    # If it is as well a PageInstance, we find the information in the
    # properties of this page. Note that we cannot distinguish here between
    # intrinsic (starting with _) and extension variables, since get_from
    # template does not know about the logic with "_" (just "property" does).
    #
    if {[$form_obj istype ::xowiki::PageInstance]} {
      #:msg "returning property $var from parent formpage $form_obj => '[$form_obj property $var $default]'"
      return [$form_obj property $var $default]
    }

    #
    # .... otherwise, it should be an instance variable ....
    #
    if {[$form_obj exists $var]} {
      #:msg "returning parent instvar [$form_obj set $var]"
      return [$form_obj set $var]
    }
    #
    # .... or, we try to resolve it against a local property.
    #
    # This case is currently needed in the workflow case, where
    # e.g. anon_instances is tried to be fetched from the first form,
    # which might not contain it, if e.g. the first form is a plain
    # wiki page.
    #
    #:msg "resolve local property $var=>[:exists_property $var]"
    if {[:istype ::xowiki::FormPage] && [:exists_property $var]} {
      #:msg "returning local property [:property $var]"
      return [:property $var]
    }
    #
    # if everything fails, return the default.
    #
    #:msg "returning the default <$default>, parent is of type [$form_obj info class]"
    return $default
  }

  PageInstance instproc render_content {} {
    set html [:get_html_from_content [:get_from_template text]]
    set html [:adp_subst $html]
    #
    # Transitional code, should be removed after the release of
    # OpenACS 5.10: In case we have a folder instances without the
    # "description" field set, and we use the new folder.form, and the
    # update script was not yet executed, folders might appear as
    # empty. In these cases, call child-resources manually.
    #
    if {$html eq "" && [:is_folder_page]} {
      ad_log_deprecated "folder page without a content" "[:item_id] '${:name}'"
      set html [:include child-resources]
    }

    return "<div class='[${:page_template} css_class_name -margin_form false]'>[:substitute_markup $html]</div>"
  }
  PageInstance instproc template_vars {content} {
    set result [list]
    foreach {_ _ v} [regexp -inline -all [template::adp_variable_regexp$content] {
      lappend result $v ""
    }

    return $result
  }

  PageInstance instproc adp_subst {content} {
    # initialize template variables (in case, new variables are added to template)
    # and add extra variables from instance attributes
    set __ia [dict merge [:template_vars $content${:instance_attributes}]

    foreach var [dict keys $__ia] {
      #:log "-- set $var [list $__ia($var)]"
      # TODO: just for the lookup, whether a field is a richt text field,
      # there should be a more efficient and easier way...
      if {[string match "richtext*" [:get_field_type $var text]]} {
        # ignore the text/html info from htmlarea
        set value [lindex [dict get $__ia $var] 0]
      } else {
        set value [dict get $__ia $var]
      }
      # the value might not be from the form attributes (e.g. title), don't clear it.
      if {$value eq "" && [info exists :$var]} continue
      set :$var [:get_field_label $var $value]
    }
    next
  }

  PageInstance instproc count_usages {
    {-package_id 0}
    {-parent_id:integer 0}
    {-publish_status ready}
  } {
    return [::xowiki::PageTemplate count_usages -package_id $package_id \
                -parent_id $parent_id -item_id ${:item_id} -publish_status $publish_status]
  }

  #
  # Methods of ::xowiki::Object
  #
  Object instproc render_content {} {
    if {[[self]::payload info methods content] ne ""} {
      set html [[self]::payload content]
      #:log render-adp=[:render_adp]
      if {[:render_adp]} {
        set html [:adp_subst $html]
        return [:substitute_markup $html]
      } else {
        #return "<pre>[string map {> &gt; < &lt;} ${:text}]</pre>"
        return $html
      }
    }
  }

  Object instproc initialize_loaded_object {} {
    :set_payload ${:text}
    next
  }

  Object instproc set_payload {cmd} {
    set payload [self]::payload
    if {[nsf::is object $payload]} {$payload destroy}
    ::xo::Context create $payload -requireNamespace \
        -actual_query [::xo::cc actual_query]
    $payload set package_id ${:package_id}
    ad_try {
      $payload contains $cmd
      $payload init
    } on error {errorMsg} {
      ad_log error "xowiki::Object set_payload: content $cmd lead to error: $errorMsg"
      ::xo::xotcl_object_cache flush ${:item_id}
    }
  }

  Object instproc get_payload {var {default ""}} {
    set payload [self]::payload
    if {![nsf::is object $payload]} {
      ::xo::Context create $payload -requireNamespace
    }
    expr {[$payload exists $var] ? [$payload set $var] : $default}
  }

  #
  # Methods of ::xowiki::Form
  #
  Form instproc footer {} {
    return [:include [list form-menu -form_item_id ${:item_id}]]
  }

  Form proc dom_disable_input_fields {{-with_submit 0} root} {
    set fields [$root selectNodes "//button | //input | //optgroup | //option | //select | //textarea "]
    set disabled [list]
    foreach field $fields {
      set type ""
      if {[$field hasAttribute type]} {set type [$field getAttribute type]}
      if {$type eq "submit" && !$with_submit} continue
      # Disabled fields are not transmitted from the form;
      # some applications expect hidden fields to be transmitted
      # to identify the context, so don't disable it...
      if {$type eq "hidden"} continue
      $field setAttribute disabled "disabled"
      if {[$field hasAttribute name]} {
        lappend disabled [$field getAttribute name]
      }
    }

    #set fa [$root selectNodes {//input[@name='__form_action']}]
    #if {$fa ne ""} {
    #  $fa setAttribute value "view-form-data"
    #}
    return $disabled
  }

  Form proc disable_input_fields {{-with_submit 0} form} {
    dom parse -html -- $form doc
    $doc documentElement root
    if {$root ne ""} {
      :dom_disable_input_fields -with_submit $with_submit $root
      set form [lindex [$root selectNodes //form] 0]
      set marginForm [::template::CSS class "margin-form"]
      if {$marginForm ne ""} {
        Form add_dom_attribute_value $form class $marginForm
      }
      return [$root asHTML]
    } else {
      ns_log notice "Form $form is apparently empty"
    }
  }

  Form proc add_dom_attribute_value {dom_node attr value} {
    if {[$dom_node hasAttribute $attr]} {
      set old_value [$dom_node getAttribute $attr]
      if {$value ni $old_value} {
        append value " " $old_value
      } else {
        set value $old_value
      }
    }
    $dom_node setAttribute $attr $value
  }

  Form instproc render_content {} {
    ::xowiki::Form requireFormCSS

    #
    # We assume that the richtext is stored as 2-element list with
    # mime-type.
    #
    #:log "-- text='${:text}'"
    if {[lindex ${:text} 0] ne ""} {
      set :do_substitutions 0
      set html ""set mime ""
      lassign ${:text} html mime
      set content [:substitute_markup $html]
    } elseif {[lindex ${:form} 0] ne ""} {
      #
      # The method "disable_input_fields" consists essentially of
      #
      #     dom parse -simple -- $form doc
      #     ...
      #     return [$root asHTML]
      #
      #  Unfortunately, this causes that some tags unknown to tdom
      #  (like <adp:icon>) are converted to escaped tags (&lt; ...).
      #  This can be regarded as a bug. To avoid this problem, we
      #  substitute here the adp_tags in advance. This needs more
      #  investigation in other cases.... The potential harm in this
      #  cases here is very little, but probably, there are other
      #  cases as well where this might harm.
      #
      set content [[self class] disable_input_fields [template::adp_parse_tags [lindex ${:form} 0]]]
    } else {
      set content ""
    }
    return $content
  }

  Form instproc get_form_constraints args {
    # We define it as a method to ease overloading.
    return [:form_constraints]
  }

  FormPage instproc create_form_fields_from_names {
    {-lookup:switch}
    {-set_values:switch}
    {-form_constraints}
    field_names
  } {
    #
    # Create form-fields from field names. When "-lookup" is
    # specified, the code tries to reuseexisting form-field instead of
    # creating/recreating it.
    #
    # Since create_raw_form_field uses destroy_on_cleanup, we do not
    # have to care here about destroying the objects.
    #
    set form_fields {}
    foreach field_name $field_names {
      if {$lookup && [:form_field_exists $field_name]} {
        #:msg "... found form_field for $field_name"
        lappend form_fields [:lookup_form_field -name $field_name {}]
      } else {
        #:msg "create '$spec_name' with spec '$short_spec'"
        lappend form_fields [:create_raw_form_field \
                                 -name $field_name \
                                 -form_constraints $form_constraints \
                                ]
      }
    }
    if {$set_values} {
      :load_values_into_form_fields $form_fields
    }
    return $form_fields
  }

  Page ad_instproc create_form_fields_from_form_constraints {
    {-lookup:switch}
    form_constraints
  } {

     Create form-fields from form constraints. When "-lookup" is
     specified, the code reuses existing form-field instead of
     recreating it.

     Since create_raw_form_field uses destroy_on_cleanup, we do not
     have to care here about destroying the objects.

     @return potentially empty list of form-field objects
   } {
    set form_fields [list]
    foreach name_and_spec $form_constraints {
      regexp {^([^:]+):(.*)$} $name_and_spec _ spec_name short_spec
      if {[string match "@table*" $spec_name]
          || $spec_name in {@categories @cr_fields}
        } continue
      if {$lookup && [:form_field_exists $spec_name]} {
        #:msg "... found form_field for $spec_name"
        lappend form_fields [:lookup_form_field -name $spec_name {}]
      } else {
        #:msg "create '$spec_name' with spec '$short_spec'"
        lappend form_fields [:create_raw_form_field \
                                 -name $spec_name \
                                 -slot [:find_slot $spec_name] \
                                 -spec $short_spec \
                                 -form_constraints $form_constraints \
                                ]
      }
    }
    return $form_fields
  }

  Page instproc validate=form_constraints {form_constraints} {
    #
    # First check for invalid meta characters for security reasons.
    #
    #if {[regexp {[\[\]]} $form_constraints]} {
    #  :uplevel [list set errorMsg [_ xowiki.error-form_constraint-invalid_characters]]
    #  return 0
    #}
    #
    # Create from fields from all specs and report, if there are any errors
    #
    ad_try {
      :create_form_fields_from_form_constraints $form_constraints
    } on error {errorMsg} {
      ad_log error "error during form_constraints validator: $errorMsg"
      :uplevel [list set errorMsg $errorMsg]
      #:log "ERROR: invalid spec '$short_spec' for form field '$spec_name' -- $errorMsg"
      return 0
    }
    return 1
  }

  Page instproc default_instance_attributes {} {
    #
    # Provide the default list of instance attributes to derived
    # FormPages.
    #
    # We want to be able to create FormPages from all pages.
    # by defining this method, we allow derived applications
    # to provide their own set of instance attributes.
    return [list]
  }

  Page instproc add_computed_instance_attributes {} {
    #
    # Provide a hook to add computed instances attributes e.g. from a
    # workflow. This method is used e.g. in form-usages for displaying
    # instance attributes in a sortable table or via csv.
    #
  }

  #
  # Methods of ::xowiki::FormPage
  #
  FormPage instproc initialize_loaded_object {} {
    #:msg "${:name} [:info class]"
    if {[info exists :page_template]} {
      set p [::xo::db::CrClass get_instance_from_db -item_id ${:page_template}]
      #
      # The Form might come from a different package type (e.g. a
      # workflow) make sure, the source package is available.
      #
      # Note that global pages (site_wide_pages) might not belong to
      # a package and have therefore an empty package_id.
      #
      set package_id [$p package_id]
      if {$package_id ne ""} {
        ::xo::Package require $package_id
      }
    }
    next
  }
  FormPage instproc initialize {} {
    # can be overloaded
  }

  FormPage instproc condition=in_state {query_context value} {
    # possible values can be or-ed together (e.g. initial|final)
    foreach v [split $value |] {
      #:msg "check [:state] eq $v"
      if {[:state] eq $v} {return 1}
    }
    return 0
  }

  FormPage ad_proc compute_filter_clauses {-unless -where} {

    Compute from "-unless" or "-where" specs the "tcl""sql" and
    optional "hstore" query fragments.

    @return dict containing "init_vars""uc" (unless clauses)
            and "wc" (where clauses)
  } {

    set init_vars [list]
    set uc {tcl false h "" vars "" sql ""}
    if {[info exists unless]} {
      set uc [dict merge $uc [:filter_expression $unless ||]]
      set init_vars [list {*}$init_vars {*}[dict get $uc vars]]
    }
    set wc {tcl true h "" vars "" sql ""}
    if {[info exists where]} {
      set wc [dict merge $wc [:filter_expression $where &&]]
      set init_vars [list {*}$init_vars {*}[dict get $wc vars]]
    }
    return [list init_vars $init_vars uc $uc wc $wc]
  }

  FormPage proc sql_value {input} {
    #
    # Transform wild-card * into SQL wild-card.
    #
    return [string map {* %} $input]
  }

  FormPage proc filter_expression {
                                   {-sql true}
                                   input_expr
                                   logical_op
                                 } {
    #ns_log notice "filter_expression '$input_expr' $logical_op"

    #
    # example for unless: wf_current_state = closed|accepted || x = 1
    #

    array set tcl_op {= eq < < > > >= >= <= <=}
    array set sql_op {= =  < < > > >= >= <= <=}
    array set op_map {
      contains,sql {$lhs_var like '%$sql_rhs%'}
      contains,tcl {{$rhs} in $lhs_var}
      matches,sql {$lhs_var like '$sql_rhs'}
      matches,tcl {[string match "$rhs" $lhs_var]}
    }

    set tcl_clause [list]
    set h_clause [list]
    set vars [list]
    set sql_clause [list]
    foreach clause [split [string map [list $logical_op \x00] $input_expr] \x00] {
      if {[regexp {^(.*[^<>])\s*([=<>]|<=|>=|contains|matches)\s*([^=]?.*)$} $clause _ lhs op rhs_expr]} {
        set lhs [string trim $lhs]
        set rhs_expr [string trim $rhs_expr]
        if {[string index $lhs 0] eq "_"} {
          #
          # Comparison with field names starting with "_"
          #
          set lhs_var [string range $lhs 1 end]
          set rhs [split $rhs_expr |]
          set sql_rhs [:sql_value $rhs]
          #:msg "check op '$op' in SQL [info exists op_map($op,sql)]"
          if {[info exists op_map($op,sql)]} {
            lappend sql_clause [subst -nocommands $op_map($op,sql)]
            if {[info exists :db_slot($lhs_var)]} {
              set lhs_var "\[set :$lhs_var\]"
              lappend tcl_clause [subst -nocommands $op_map($op,tcl)]
            } else {
              :msg "ignoring unknown variable '$lhs_var' in expression (have '[lsort [array names :db_slot]]')"
            }
          } elseif {[llength $rhs]>1} {
            lappend sql_clause "$lhs_var in ([ns_dbquotelist $rhs])"
            # the following statement is only needed, when we rely on tcl-only
            lappend tcl_clause "\[lsearch -exact {$rhs} \[:property $lhs\]\] > -1"
          } else {
            lappend sql_clause "$lhs_var $sql_op($op) '$rhs'"
            # the following statement is only needed, when we rely on tcl-only
            lappend tcl_clause "\[:property $lhs\] $tcl_op($op) {$rhs}"
          }
        } else {
          #
          # Field names referring to instance attributes.
          #
          set hleft [::xowiki::hstore::double_quote $lhs]
          lappend vars $lhs ""
          if {$op eq "contains"} {
            #make approximate query
            set lhs_var instance_attributes
            set sql_rhs $rhs_expr
            lappend sql_clause [subst -nocommands $op_map($op,sql)]
          }
          set lhs_var "\[dict get \$__ia $lhs\]"
          set tcl_rhs_clauses {}
          foreach rhs [split $rhs_expr |] {
            set sql_rhs [:sql_value $rhs]
            if {[info exists op_map($op,tcl)]} {
              lappend tcl_rhs_clauses [subst -nocommands $op_map($op,tcl)]
            } else {
              lappend tcl_rhs_clauses "$lhs_var $tcl_op($op) {$rhs}"
            }
            if {$op eq "="} {
              # TODO: think about a solution for other operators with
              # hstore maybe: extracting it by a query via hstore and
              # compare in plain SQL
              lappend h_clause "$hleft=>[::xowiki::hstore::double_quote $rhs]"
            }
          }
          lappend tcl_clause ([join $tcl_rhs_clauses ||])
        }
      } else {
        :msg "ignoring $clause"
      }
    }
    if {[llength $tcl_clause] == 0} {
      set tcl_clause [list true]
    }
    #:msg sql=$sql_clause,tcl=$tcl_clause
    set result [list \
                    tcl [join $tcl_clause $logical_op] \
                    h [join $h_clause ,] \
                    vars $vars \
                    sql $sql_clause]
    #:msg "filter_expression -sql $sql inp '$input_expr' log '$logical_op' -> $result"

    return $result
  }

  FormPage proc get_form_entries {
                                  -base_item_ids:required
                                  -package_id:required
                                  -form_fields:required
                                  {-publish_status ready}
                                  {-parent_id "*"}
                                  {-extra_where_clause ""}
                                  {-h_where {tcl true h "" vars "" sql ""}}
                                  {-h_unless {tcl true h "" vars "" sql ""}}
                                  {-always_queried_attributes ""}
                                  {-orderby ""}
                                  {-page_size 20}
                                  {-page_number ""}
                                  {-initialize true}
                                  {-from_package_ids ""}
                                } {
    #
    # Get query attributes for all tables (to allow e.g. sorting by time)
    #
    # The basic essential fields item_id, name, object_type and
    # publish_status are always automatically fetched from the
    # instance_select_query. Add the query attributes, we want to
    # obtain as well automatically.
    #
    # "-parent_id *"  means to get instances, regardless of
    # parent_id. Under the assumption, page_template constrains
    # the query enough to make it fast...
    #
    # "-from_package_ids {}" means get pages from the instance
    # provided via package_id, "*" means from all
    # packages. Furthermore, a list of package_ids can be given.
    #
    # "-always_queried_attributes *" means to obtain enough attributes
    # to allow a save operations etc. on the instances.
    #

    set sql_atts {
      item_id name publish_status object_type
      parent_id revision_id instance_attributes
      creation_date creation_user last_modified
      package_id title page_template state assignee
    }

    if {$always_queried_attributes eq "*"} {
      lappend sql_atts \
          object_type object_id \
          description publish_date mime_type nls_language text \
          creator page_order page_id \
          page_instance_id xowiki_form_page_id
    } else {
      foreach att $always_queried_attributes {
        set name [string range $att 1 end]
        lappend sql_atts $name
      }
    }

    #
    # Compute the list of field_names from the already covered sql
    # attributes
    #
    set covered_attributes [list _name _publish_status _item_id _object_type]
    foreach att $sql_atts {
      #regexp {[.]([^ ]+)} $att _ name
      lappend covered_attributes _$att
    }

    #
    # Collect SQL attributes from form_fields
    #
    foreach f $form_fields {
      if {![$f exists __base_field]} continue
      set field_name [$f name]
      if {$field_name in $covered_attributes} {
        continue
      }
      lappend sql_atts [$f set __base_field]
    }
    #:msg sql_atts=$sql_atts

    #
    # Build parts of WHERE clause
    #
    set publish_status_clause [::xowiki::Includelet publish_status_clause \
                                   -base_table "" $publish_status]
    #
    # Build filter clause (uses hstore if configured)
    #
    set filter_clause ""
    array set wc $h_where
    array set uc $h_unless
    set use_hstore [expr {[::xo::dc has_hstore] &&
                          [::$package_id get_parameter use_hstore:boolean 0]
                        }]
    #
    # Deactivating hstore optimization for now, must be further
    # completed and debugged before activating it again.
    #
    if {$wc(h) ne "" || $uc(h) ne ""} {
      ns_log notice "hstore available $use_hstore, but deactivating anyway for now (wc $wc(h) uc $uc(h) )"
    }

    set use_hstore 0
    if {$use_hstore} {
      if {$wc(h) ne ""} {
        set filter_clause " and '$wc(h)' <@ hkey"
      }
      if {$uc(h) ne ""} {
        set filter_clause " and not '$uc(h)' <@ hkey"
      }
    }
    if {$wc(sql) ne ""} {
      #:log "... wc SQL '$wc(sql)'"
      foreach filter $wc(sql) {
        append filter_clause " and $filter"
      }
    }
    if {$uc(sql) ne ""} {
      #:log "... uc SQL '$uc(sql)'"
      foreach filter $uc(sql) {
        append filter_clause " and not $filter"
      }
    }
    #:log filter_clause=$filter_clause

    #
    # Build package clause
    #
    if {$from_package_ids eq ""} {
      set package_clause "and package_id = :package_id"
    } elseif {$from_package_ids eq "*"} {
      set package_clause ""
    } elseif {[llength $from_package_ids] == 1} {
      set package_clause "and package_id = :from_package_ids"
    } else {
      set package_clause "and package_id in ([ns_dbquotelist $from_package_ids])"
    }

    if {$parent_id eq "*"} {
      # instance_select_query expects "" for all parents, but for the semantics
      # of this method, "*" looks more appropriate
      set parent_id ""
    }

    set parent_clause ""
    if {$parent_id ne ""} {
      set parent_clause " and parent_id = :parent_id"
    }

    if {[llength $base_item_ids] == 0} {
      error "base_item_ids must not be empty"
    }
    #
    # transform all into an SQL query
    #
    if {$page_number ne ""} {
      set limit $page_size
      set offset [expr {$page_size*($page_number-1)}]
    } else {
      set limit ""
      set offset ""
    }
    set sql [::xo::dc select \
                 -vars [join $sql_atts ", "] \
                 -from xowiki_form_instance_item_view \
                 -where " page_template in ([ns_dbquotelist $base_item_ids]) \
            $publish_status_clause $filter_clause $package_clause $parent_clause \
            $extra_where_clause" \
                 -orderby $orderby \
                 -limit $limit -offset $offset]
    #ns_log notice "get_form_entries:\n[string map [list :parent_id $parent_id :package_id $package_id] $sql]"

    #
    # When we query all attributes, we return objects named after the
    # item_id (like for single fetches)
    #
    set named_objects [expr {$always_queried_attributes eq "*"}]
    set items [::xowiki::FormPage instantiate_objects -sql $sql \
                   -named_objects $named_objects -object_named_after "item_id" \
                   -object_class ::xowiki::FormPage -initialize $initialize]

    #:log "$use_hstore wc tcl $wc(tcl) uc tcl $uc(tcl)"
    if {!$use_hstore && ($wc(tcl) != "true" || $uc(tcl) != "true")} {

      set init_vars $wc(vars)
      foreach p [$items children] {
        $p set __ia [dict merge $init_vars [$p instance_attributes]]

        if {$wc(tcl) != "true"} {
          if {![nsf::directdispatch $p -frame object ::expr $wc(tcl)]} {
            #:log "WC check '$wc(tcl)' [$p name] => where DELETE"
            $items delete $p
            continue
          }
        }
        if {$uc(tcl) != "true"} {
          if {[nsf::directdispatch $p -frame object ::expr $uc(tcl)]} {
            #:log "UC check '$uc(tcl)' on [$p name] => unless DELETE"
            $items delete $p
          }
        }
      }
    }
    return $items
  }

  FormPage proc get_folder_children {
                                     -folder_id:required
                                     {-publish_status ready}
                                     {-object_types {::xowiki::Page ::xowiki::Form ::xowiki::FormPage}}
                                     {-extra_where_clause true}
                                     {-initialize true}
                                   } {
    set publish_status_clause [::xowiki::Includelet publish_status_clause $publish_status]
    set result [::xo::OrderedComposite new -destroy_on_cleanup]

    foreach object_type $object_types {
      set attributes [list revision_id creation_user title parent_id page_order \
                          "to_char(last_modified,'YYYY-MM-DD HH24:MI') as last_modified" ]
      set base_table [$object_type set table_name]i
      if {$object_type eq "::xowiki::FormPage"} {
        set attributes "bt.* $attributes"
      }
      set items [$object_type get_instances_from_db \
                     -folder_id $folder_id \
                     -with_subtypes false \
                     -select_attributes $attributes \
                     -where_clause "$extra_where_clause $publish_status_clause" \
                     -base_table $base_table \
                     -initialize $initialize]

      foreach i [$items children] {
        $result add $i
      }
    }
    return $result
  }

  FormPage proc get_super_folders {package_id folder_id {aggregated_folder_refs ""}} {
    #
    # Compute the set of folder_refs configured in the referenced
    # folders.  Get first the folder_refs configured in the actual
    # folder, which are not yet in aggregated_folder_refs.
    #
    set additional_folder_refs ""
    set folder [::xo::db::CrClass get_instance_from_db -item_id $folder_id -revision_id 0]
    if {[$folder istype ::xowiki::FormPage]} {
      foreach ref [$folder property inherit_folders] {
        if {$ref ni $aggregated_folder_refs} {lappend additional_folder_refs $ref}
      }
    }
    #
    # Process the computed additional folder refs recursively to obtain
    # the transitive set of configured item_refs (pointing to folders).
    #
    lappend aggregated_folder_refs {*}$additional_folder_refs
    foreach item_ref $additional_folder_refs {
      set page [::$package_id get_page_from_item_ref $item_ref]
      if {$page eq ""} {error "configured inherited folder $item_ref cannot be resolved"}
      set aggregated_folder_refs \
          [FormPage get_super_folders $package_id [$page item_id] $aggregated_folder_refs]
    }
    return $aggregated_folder_refs
  }

  FormPage proc get_all_children {
                                  -folder_id:required
                                  {-publish_status ready}
                                  {-object_types {::xowiki::Page ::xowiki::Form ::xowiki::FormPage}}
                                  {-extra_where_clause "1=1"}
                                  {-include_child_folders none}
                                  {-initialize true}
                                } {

    set folder [::xo::db::CrClass get_instance_from_db -item_id $folder_id -revision_id 0]
    set package_id [$folder package_id]

    set publish_status_clause [::xowiki::Includelet publish_status_clause $publish_status]
    set result [::xo::OrderedComposite new -destroy_on_cleanup]
    $result set folder_ids ""

    set list_of_folders [list $folder_id]
    set inherit_folders [FormPage get_super_folders $package_id $folder_id]
    #:log inherit_folders=$inherit_folders

    foreach item_ref $inherit_folders {
      set folder [::xo::cc cache [list ::$package_id get_page_from_item_ref $item_ref]]
      if {$folder eq ""} {
        ad_log error "Could not resolve parameter folder page '$item_ref' of FormPage [self]."
      } else {
        lappend list_of_folders [$folder item_id]
      }
    }

    if {$include_child_folders eq "direct"} {
      #
      # Get all children of the current folder on the first level and
      # append it to the list_of_folders.
      #
      set folder_form [::$package_id instantiate_forms -forms en:folder.form]
      set child_folders [xo::dc list -prepare integer,integer get_child_folders {
        select item_id from xowiki_form_instance_item_index
        where parent_id = :folder_id
        and page_template = :folder_form
      }]
      foreach f $child_folders {
        ::xo::db::CrClass get_instance_from_db -item_id $f
      }
      lappend list_of_folders {*}$child_folders
    }

    $result set folder_ids $list_of_folders

    foreach folder_id $list_of_folders {
      foreach object_type $object_types {
        set attributes [list revision_id creation_user title parent_id page_order \
                            "to_char(last_modified,'YYYY-MM-DD HH24:MI') as last_modified" ]
        set base_table [$object_type set table_name]i
        if {$object_type eq "::xowiki::FormPage"} {
          set attributes "bt.* $attributes"
        }
        set items [$object_type get_instances_from_db \
                       -folder_id $folder_id \
                       -with_subtypes false \
                       -initialize $initialize \
                       -select_attributes $attributes \
                       -where_clause "$extra_where_clause $publish_status_clause" \
                       -base_table $base_table]

        foreach i [$items children] {
          $result add $i
        }
      }
    }
    return $result
  }

  # part of the code copied from Package->get_parameter
  # see xowiki/www/prototypes/folder.form.page
  FormPage instproc get_parameter {attribute {default ""}} {
    #
    # Try to get the parameter from the parameter_page provided as
    # property "ParameterPages".
    #
    set value [::${:package_id} get_parameter_from_parameter_page \
                   -parameter_page_name [:property ParameterPages] \
                   $attribute]
    if {$value eq {}} {set value [next $attribute $default]}
    return $value
  }

  #
  # begin property management
  #

  #FormPage instproc property_key {name} {
  #  if {[regexp {^_([^_].*)$} $name _ varname]} {
  #    return $varname
  #  } {
  #    return __ia($name)
  #  }
  #}

  FormPage instproc exists_property {name} {
    if {[regexp {^_([^_].*)$} $name _ varname]} {
      return [info exists :$varname]
    }
    return [dict exists ${:instance_attributes} $name]
  }

  FormPage ad_instproc property {
    name
    {default ""}
  } {
    Retrieve a FormPage property.

    @param name property name. Names starting with _ refer to object's
                members, rather than instance attributes.
    @param default fallback value when property is not set.
  } {
    if {[regexp {^_([^_].*)$} $name _ varname]} {
      if {[info exists :$varname]} {
        return [set :$varname]
      }
    } elseif {[dict exists ${:instance_attributes} $name]} {
      return [dict get ${:instance_attributes} $name]
    }
    return $default
  }

  FormPage ad_instproc set_property {
    {-new 0}
    name
    value
  } {
    Stores a value as FormPage property

    @param new boolean flag telling if the property is new. Setting a
               value on a non-existing property without specifying
               this flag will result in an error.
    @param name property name. Names starting with _ indicate an
                object variable rather than a property stored in
                instance_attributes
    @param value property value

    @return value (eventually converted to a has-notation message key)
  } {
    if {[string match "_*" $name]} {
      set key [string range $name 1 end]

      if {!$new && ![info exists :$key]} {
        error "property '$name' ($key) does not exist. \
        you might use flag '-new 1' for set_property to create new properties"
      }
      set :$key $value

    } else {

      if {!$new && ![dict exists ${:instance_attributes} $name]} {
        error "property '$name' does not exist. \
        you might use flag '-new 1' for set_property to create new properties"
      }
      dict set :instance_attributes $name $value
    }
    return $value
  }

  FormPage ad_instproc get_property {
    -source
    -name:required
    {-default ""}
  } {
    Retrieves a FormPage property

    @param source page name to be resolved and used instead this
                  FormPage to fetch the property
  } {
    if {![info exists source]} {
      set page [self]
    } else {
      set page [:resolve_included_page_name $source]
    }
    return [$page property $name $default]
  }

  FormPage instproc lappend_property {name value} {
    #
    # lappend the specified value to the named property. If the
    # property does not exists, create a new one.
    #
    if {[:exists_property $name]} {
      :set_property $name [concat [:get_property -name $name$value]
    } else {
      :set_property -new 1 $name $value
    }
  }

  FormPage instproc condition=is_true {query_context value} {
    #
    # This condition maybe called from the policy rules.
    # The passed value is a tuple of the form
    #     {property-name operator property-value}
    #
    lassign $value property_name op property_value
    if {![info exists property_value]} {
      return 0
    }

    #:log "$value => [:adp_subst $value]"
    array set wc [::xowiki::FormPage filter_expression [:adp_subst $value] &&]
    #:log "wc= [array get wc]"
    set __ia [dict merge $wc(vars) [:instance_attributes]]
    #:log "expr $wc(tcl) returns => [expr $wc(tcl)]"
    return [expr $wc(tcl)]
  }

  # If the folder has a property "langstring" assume that the
  # content is a dict containing multiple attributes in multiple
  # languages.
  #
  #    _title {en {This is the Title} de {Das ist der Titel}}
  #
  # This can be used by update_langstring_property to set arbitaries
  # properties to language-specific value. The follogwing command updates
  # the value of the "_title" property:
  #
  #    $page update_langstring_property _title $lang
  #
  # One should define a form-field for langstrings that convert
  # some user-friendly format into the intrep of the dict, which
  # can be efficiently processed.

  FormPage instproc langstring {attname lang {default ""}} {
    set result $default
    if {[:exists_property langstring]} {
      set d [:property langstring]
      if {[dict exists $d $attname $lang]} {
        set result [dict get $d $attname $lang]
      }
    }
    return $result
  }
  FormPage instproc update_langstring_property {attname lang} {
    :set_property $attname [:langstring $attname $lang [:property $attname]]
  }

  #
  # end property management
  #

  FormPage instproc set_publish_status {value} {
    if {$value ni {production ready}} {
      error "invalid value '$value'; use 'production' or 'ready'"
    }
    set :publish_status $value
  }

  FormPage instproc footer {} {
    if {[info exists :__no_form_page_footer]} {
      next
    } else {
      set is_form [:property is_form__ 0]
      if {[:is_form]} {
        return [:include [list form-menu -form_item_id ${:item_id} \
                              -buttons [list new answers [list form ${:page_template}]]]]
      } else {
        return [:include [list form-menu -form_item_id ${:page_template} -buttons form]]
      }
    }
  }


  FormPage instproc field_names_from_form {{-form ""}} {
    #
    # This method returns the form attributes (including _*).
    #
    set allvars [list {*}[[:info class] array names db_slot] \
                     {*}[::xo::db::CrClass set common_query_atts]]

    set template [:get_html_from_content [:get_from_template text]]
    #:msg template=$template

    #set field_names [list _name _title _description _creator _nls_language _page_order]
    set field_names [list]
    if {$form eq ""} {set form [:get_form]}
    if {$form eq ""} {
      foreach {var _} [:template_vars $template] {
        #if {[string match _* $var]} continue
        if {$var ni $allvars && $var ni $field_names} {
          lappend field_names $var
        }
      }
      set from_HTML_form 0
    } else {
      foreach {match 1 att} [regexp -all -inline [template::adp_variable_regexp$form] {
        #if {[string match _* $att]} continue
        lappend field_names $att
      }
      #ns_log notice "field_names_from_form: [:serialize]"
      dom parse -html -- $form doc
      $doc documentElement root
      set fields [$root selectNodes "//*\[@name != ''\]"]
      foreach field $fields {
        set node_name [$field nodeName]
        if {$node_name ne "input"
            && $node_name ne "textarea"
            && $node_name ne "select"
          } continue
        set att [$field getAttribute name]
        #if {[string match _* $att]} continue
        if {$att ni $field_names} { lappend field_names $att }
      }
      set from_HTML_form 1
    }
    return [list $from_HTML_form $field_names]
  }

  Page instproc render_icon {} {
    return [list text [namespace tail [:info class]] is_richtext false]
  }

  File instproc render_icon {} {
    return {text "<a title='file' class='file-icon'>&nbsp;</a>" is_richtext true}
  }

  FormPage instproc render_icon {} {
    set page_template ${:page_template}
    if {[$page_template istype ::xowiki::FormPage]} {
      return [list text [$page_template property icon_markup] is_richtext true]
    }
    switch [$page_template name] {
      en:folder.form {
        return {text "<a title='folder' class='folder-open-icon'>&nbsp;</a>" is_richtext true}
      }
      en:link.form {
        set link_type [:get_property_from_link_page link_type "unresolved"]
        if {$link_type eq "unresolved"} {
          return {text "<a title='broken link' class='broken-link-icon'>&nbsp;</a>" is_richtext true}
        } else {
          return {text "<a title='link' class='link-icon'>&nbsp;</a>" is_richtext true}
        }
      }
      default {
        return [list text [$page_template title] is_richtext false]
      }
    }
  }

  Page instproc pretty_name {} {
    return ${:name}
  }

  FormPage instproc pretty_name {} {
    set anon_instances [:get_from_template anon_instances f]
    if {$anon_instances} {
      return ${:title}
    }
    return ${:name}
  }

  File instproc pretty_name {} {
    set name ${:name}
    regsub {^file:} $name "" name
    return $name
  }

  FormPage instproc include_header_info {{-prefix ""} {-js ""} {-css ""}} {
    if {$css eq ""} {set css [:get_from_template ${prefix}_css]}
    if {$js eq ""}  {set js [:get_from_template ${prefix}_js]}
    foreach line [split $js \n] {
      set line [string trim $line]
      if {$line ne ""} {
        ::xo::Page requireJS $line
      }
    }
    foreach line [split $css \n] {
      set line [string trim $line]
      if {$line eq ""} continue
      set order 1
      if {[llength $line]>1} {
        set e1 [lindex $line 0]
        if {[string is integer -strict $e1]} {
          set order $e1
          set line [lindex $line 1]
        }
      }
      ::xo::Page requireCSS -order $order $line
    }
  }

  FormPage instproc render_content {} {
    #
    # Produce an HTML rendering from the FormPage.
    #
    #set package_id ${:package_id}
    :include_header_info -prefix form_view
    if {[::xo::cc mobile]} {
      :include_header_info -prefix mobile
    }
    set text [:get_from_template text]
    if {$text ne ""} {
      catch {set text [lindex $text 0]}
    }
    if {$text ne ""} {
      #:log "we have a template text='$text'"
      #
      # We have a template, this is the first preference.
      #
      set HTML [next]
    } else {
      #:log "we have a form '[:get_form]'"
      #
      # Fall back to the form, fill it out and compute HTML from this.
      #
      set form [:get_form]
      if {$form eq ""} {
        return ""
      }

      lassign [:field_names_from_form -form $form] form_vars field_names
      set :__field_in_form ""
      if {$form_vars} {
        foreach v $field_names {
          dict set :__field_in_form $v 1
        }
      }
      set form_fields [:create_form_fields $field_names]
      foreach n $field_names f $form_fields {
        dict set :__form_fields $n $f
      }

      :load_values_into_form_fields $form_fields

      # deactivate form-fields and do some final sanity checks
      foreach f $form_fields {$f set_disabled 1}
      :form_fields_sanity_check $form_fields
      :post_process_form_fields $form_fields

      set form [:regsub_eval  \
                    [template::adp_variable_regexp$form \
                    {:form_field_as_html -mode display "\\\1" "\2" $form_fields}]

      # we parse the form just for the margin-form.... maybe regsub?
      dom parse -html -- $form :doc
      ${:doc} documentElement :root
      set form_node [lindex [${:root} selectNodes //form] 0]

      Form add_dom_attribute_value $form_node role form
      Form add_dom_attribute_value $form_node class [${:page_template} css_class_name]
      # The following two commands are for non-generated form contents
      :set_form_data $form_fields
      Form dom_disable_input_fields ${:root}
      # Return finally the result
      set HTML [${:root} asHTML]
    }

    return $HTML
  }

  FormPage instproc get_value {{-field_spec ""} {-cr_field_spec ""} before varname} {
    #
    # Read a property (instance attribute) and return
    # its pretty value in variable substitution.
    #
    # We check for special variable names here (such as current_user
    # or current_url). We provide a value from the current connection
    # context.
    if {$varname eq "current_user"} {
      set value [::xo::cc set untrusted_user_id]

    } elseif {$varname eq "current_url"} {
      set value [::xo::cc url]

    } else {
      #
      # First check to find an existing form-field with that name
      #
      set f [::xowiki::formfield::FormField get_from_name [self$varname]
      if {$f ne ""} {
        #
        # The form field exists already, we just fill in the actual
        # value (needed e.g. in weblogs, when the same form field is
        # used for multiple page instances in a single request)
        #
        set value [$f value [:property $varname]]
      } else {
        #
        # create a form-field from scratch
        #
        set value [:property $varname]
        set f [:create_form_field -cr_field_spec $cr_field_spec -field_spec $field_spec $varname]
        $f value $value
      }

      if {[$f hide_value]} {
        set value ""
      } elseif {![$f exists show_raw_value]} {
        set value [$f pretty_value $value]
      }
    }
    return $before$value
  }

  FormPage instproc adp_subst {content} {
    # Get the default field specs once and pass it to every field creation
    set field_spec [:get_short_spec @fields]
    set cr_field_spec [:get_short_spec @cr_fields]
    # Iterate over the variables for substitution
    set content [:regsub_eval -noquote true \
                     [template::adp_variable_regexp$content" \
                     {:get_value -field_spec $field_spec -cr_field_spec $cr_field_spec "\\\1" "\2"}]
    return [string range $content 1 end]
  }

  FormPage instproc group_require {} {
    #
    # Create a group if necessary associated to the current form
    # page. Since the group_names are global, the group name contains
    # the parent_id of the FormPage.
    #
    set group_name "fpg-${:parent_id}-${:name}"
    set group_id [group::get_id -group_name $group_name]
    if {$group_id eq ""} {
      # group::new does not flush the cache - sigh!  Therefore, we have
      # to flush the old cache entry here manually.
      ::acs::clusterwide ns_cache flush util_memoize \
          "group::get_id_not_cached -group_name $group_name -subsite_id {} -application_group_id {}"
      set group_id [group::new -group_name $group_name]
    }
    return $group_id
  }

  FormPage instproc group_assign {
    -group_id:integer,required
    -members:required
    {-rel_type membership_rel}
    {-member_state ""}
  } {
    set old_members [group::get_members -group_id $group_id]
    foreach m $members {
      if {$m ni $old_members} {
        #:msg "we have to add $m"
        group::add_member -group_id $group_id -user_id $m \
            -rel_type $rel_type -member_state $member_state
      }
    }
    foreach m $old_members {
      if {$m ni $members} {
        #:msg "we have to remove $m"
        group::remove_member -group_id $group_id -user_id $m
      }
    }
  }


  Page instproc is_new_entry {old_name} {
    return [expr {${:publish_status} eq "production" && $old_name eq ${:revision_id}}]
  }

  Page instproc unset_temporary_instance_variables {} {
    #
    # Don't marshall/save/cache the following vars:
    #
    # array unset :__ia
    unset -nocomplain :__form_fields :__field_in_form  :__field_needed
  }

  Page instproc map_categories {category_ids} {
    #
    # Could be optimized, if we do not want to have categories (form
    # constraints?)
    #
    #:log "--category::map_object -remove_old -object_id ${:item_id} <$category_ids>"
    category::map_object -remove_old -object_id ${:item_id} $category_ids
  }

  Page instproc rename {-old_name -new_name} {
    ${:package_id} flush_name_cache -name $old_name -parent_id ${:parent_id}
    next
    :log "----- rename <$old_name> to <$new_name>"
    #ns_log notice [:serialize]
  }

  #
  # The method save_data is called typically via www-callable methods
  # and has some similarity to "new_data" and "edit_data" in
  # "ad_forms". It performs some updates in an instance (e.g. caused
  # by categories), saves the data and calls finally the notification
  # procs.
  #
  Page instproc save_data {{-use_given_publish_date:boolean false} old_name category_ids} {
    #:log "-- [self args]"
    :unset_temporary_instance_variables
    set package_id ${:package_id}

    ::xo::dc transaction {
      #
      # If the newly created item was in production mode, but ordinary entries
      # are not, change on the first save the status to ready
      #
      #ns_log notice "----- save_data: old_name $old_name, is_new_entry [:is_new_entry $old_name] name <${:name}>"
      if {[:is_new_entry $old_name]} {
        if {![::$package_id get_parameter production_mode:boolean 0]} {
          set :publish_status "ready"
        }
      }
      :map_categories $category_ids

      #
      # Handle now further database operations that should be saved in
      # a transaction. Examples are calendar-items defined in a
      # FormPage, which should show up also in the calendar.
      #
      # Probably, categories should also be moved into the
      # transaction queue.
      #
      set queue ::__xowiki__transaction_queue(${:item_id})
      if {[info exists $queue]} {
        foreach cmd [set $queue] {
          #ns_log notice ".... executing transaction command: $cmd"
          {*}$cmd
        }
      }

      :save -use_given_publish_date $use_given_publish_date
      if {$old_name ne ${:name}} {
        :rename -old_name $old_name -new_name ${:name}
      }
      :notification_notify
    }
    return ${:item_id}
  }

}

::xo::library source_dependent

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