<commit_before>/**
 * Copyright (C) 2011 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xforms.event

import events._
import org.orbeon.oxf.webapp.SessionExpiredException
import org.orbeon.oxf.logging.LifecycleLogger
import org.orbeon.oxf.xforms.control._
import org.orbeon.oxf.xforms.control.controls._
import org.orbeon.oxf.xforms.XFormsConstants._
import org.orbeon.oxf.xforms.XFormsContainingDocument
import org.orbeon.oxf.xforms.XFormsUtils._
import org.orbeon.oxf.xml._
import java.{util ⇒ ju}
import dom4j.{LocationSAXContentHandler, Dom4jUtils}
import org.orbeon.oxf.pipeline.api._
import org.dom4j.{Document, Element}
import org.orbeon.oxf.xforms.state.XFormsStateManager
import org.orbeon.oxf.util.{NetUtils, IndentedLogger, Multipart, Logging}
import XFormsEvents._
import collection.JavaConverters._
import org.orbeon.oxf.xforms.analysis.controls.RepeatControl
import org.orbeon.oxf.xforms.event.XFormsEvent._

// Process events sent by the client, including sorting, filtering, and security
object ClientEvents extends Logging with XMLReceiverSupport {

  // Only a few events specify custom properties that can be set by the client
  private val AllStandardProperties =
    XXFormsDndEvent.StandardProperties        ++
    KeypressEvent.StandardProperties          ++
    XXFormsUploadDoneEvent.StandardProperties ++
    XXFormsLoadEvent.StandardProperties

  private val DummyEvent = List(LocalEvent(Dom4jUtils.createElement("dummy"), trusted = false))

  case class LocalEvent(private val element: Element, trusted: Boolean) {

    def attributeValue(name: String) = element.attributeValue(name)
    def elementForDebug = element

    val name              = attributeValue("name")
    val targetEffectiveId = attributeValue("source-control-id")
    val bubbles           = attributeValue("bubbles")    != "false" // default is true
    val cancelable        = attributeValue("cancelable") != "false" // default is true

    lazy val properties   = Dom4j.elements(element, XXFORMS_PROPERTY_QNAME) map { e ⇒ (e.attributeValue("name"), Option(e.getText)) } toMap
    lazy val value        = if (properties.nonEmpty) "" else element.getText // for now we don't support both a value and properties
  }

  def extractLocalEvents(actionElement: Element): List[LocalEvent] =
    if (actionElement ne null)
      Dom4j.elements(actionElement, XXFORMS_EVENT_QNAME) map (LocalEvent(_, trusted = false)) toList
    else
      Nil

  def extractServerEventsElements(rootElement: Element) =
    Dom4j.elements(rootElement, XXFORMS_SERVER_EVENTS_QNAME) toList

  // Entry point called by the server: process a sequence of incoming client events.
  def processEvents(
    doc                  : XFormsContainingDocument,
    clientEvents         : List[LocalEvent],
    serverEventsElements : List[Element]
  ): (Boolean, ju.Set[String], Option[String]) = {

    val allClientAndServerEvents = {

      // Process events for noscript mode if needed
      val clientEventsAfterNoscript =
        if (doc.noscript)
          reorderNoscriptEvents(clientEvents, doc)
        else
          clientEvents

      // Decode encrypted server events
      def decodeServerEvents(text: String) =
        Dom4j.elements(decodeXML(text).getRootElement, XXFORMS_EVENT_QNAME) map
          (LocalEvent(_, trusted = true)) toList

      // All global server events
      val globalServerEvents = serverEventsElements flatMap (e ⇒ decodeServerEvents(e.getStringValue))

      // Gather all events including decoding action server events
      globalServerEvents ++
        (clientEventsAfterNoscript flatMap {
          case event if event.name == XXFORMS_SERVER_EVENTS ⇒
            decodeServerEvents(event.value)
          case event ⇒
            List(event)
        })
    }

    if (allClientAndServerEvents.nonEmpty) {

      def filterEvents(events: List[LocalEvent]) = events filter {
        case a if a.name == XXFORMS_ALL_EVENTS_REQUIRED ⇒ false
        case a if (a.name eq null) || (a.targetEffectiveId eq null) ⇒
          debug("ignoring invalid client event", Seq(
            "control id" → a.targetEffectiveId,
            "event name" → a.name)
          )(doc.indentedLogger)
          false
        case _ ⇒ true
      }

      def combineValueEvents(events: List[LocalEvent]): List[XFormsEvent] = events match {
        case Nil              ⇒ Nil
        case List(localEvent) ⇒ safelyCreateAndMapEvent(doc, localEvent).toList
        case _                ⇒

          // Grouping key for value change events
          case class EventGroupingKey(name: String, targetId: String) {
            def this(localEvent: LocalEvent) =
              this(localEvent.name, localEvent.targetEffectiveId)
          }

          // Slide over the events so we can filter and compress them
          // NOTE: Don't use Iterator.toSeq as that returns a Stream, which evaluates lazily. This would be great, except
          // that we *must* first create all events, then dispatch them, so that references to XFormsTarget are obtained
          // beforehand.
          (events ++ DummyEvent).sliding(2).toList flatMap {
            case List(a, b) ⇒
              if (a.name != XXFORMS_VALUE || new EventGroupingKey(a) != new EventGroupingKey(b))
                safelyCreateAndMapEvent(doc, a)
              else
                None
          }
      }

      // Combine and process events
      for (event ← combineValueEvents(filterEvents(allClientAndServerEvents)))
        processEvent(doc, event)

      // Gather some metadata about the events received to help with the response to the client

      // Whether we got a request for all events
      val gotAllEvents = allClientAndServerEvents exists
        (_.name == XXFORMS_ALL_EVENTS_REQUIRED)

      // Set of all control ids for which we got value events
      val valueChangeControlIds = allClientAndServerEvents collect
        { case e if e.name == XXFORMS_VALUE ⇒ e.targetEffectiveId } toSet

      // Last client focus/blur event received
      val clientFocusControlId = allClientAndServerEvents.reverse collectFirst {
        case e if e.name == XFORMS_FOCUS ⇒ Some(e.targetEffectiveId)
        case e if e.name == XXFORMS_BLUR ⇒ None
      }

      (gotAllEvents, valueChangeControlIds.asJava, clientFocusControlId.orNull)

    } else
      (false, ju.Collections.emptySet[String], null)
  }

  // NOTE: Leave public for unit tests
  def reorderNoscriptEvents(eventElements: List[LocalEvent], doc: XFormsContainingDocument): List[LocalEvent] = {<commit_msg>Fix #2567 "Delayed xforms-focus event incorrectly checks for client focus"<commit_after>/**
 * Copyright (C) 2011 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xforms.event

import events._
import org.orbeon.oxf.webapp.SessionExpiredException
import org.orbeon.oxf.logging.LifecycleLogger
import org.orbeon.oxf.xforms.control._
import org.orbeon.oxf.xforms.control.controls._
import org.orbeon.oxf.xforms.XFormsConstants._
import org.orbeon.oxf.xforms.XFormsContainingDocument
import org.orbeon.oxf.xforms.XFormsUtils._
import org.orbeon.oxf.xml._
import java.{util ⇒ ju}
import dom4j.{LocationSAXContentHandler, Dom4jUtils}
import org.orbeon.oxf.pipeline.api._
import org.dom4j.{Document, Element}
import org.orbeon.oxf.xforms.state.XFormsStateManager
import org.orbeon.oxf.util.{NetUtils, IndentedLogger, Multipart, Logging}
import XFormsEvents._
import collection.JavaConverters._
import org.orbeon.oxf.xforms.analysis.controls.RepeatControl
import org.orbeon.oxf.xforms.event.XFormsEvent._

// Process events sent by the client, including sorting, filtering, and security
object ClientEvents extends Logging with XMLReceiverSupport {

  // Only a few events specify custom properties that can be set by the client
  private val AllStandardProperties =
    XXFormsDndEvent.StandardProperties        ++
    KeypressEvent.StandardProperties          ++
    XXFormsUploadDoneEvent.StandardProperties ++
    XXFormsLoadEvent.StandardProperties

  private val DummyEvent = List(LocalEvent(Dom4jUtils.createElement("dummy"), trusted = false))

  case class LocalEvent(private val element: Element, trusted: Boolean) {

    def attributeValue(name: String) = element.attributeValue(name)
    def elementForDebug = element

    val name              = attributeValue("name")
    val targetEffectiveId = attributeValue("source-control-id")
    val bubbles           = attributeValue("bubbles")    != "false" // default is true
    val cancelable        = attributeValue("cancelable") != "false" // default is true

    lazy val properties   = Dom4j.elements(element, XXFORMS_PROPERTY_QNAME) map { e ⇒ (e.attributeValue("name"), Option(e.getText)) } toMap
    lazy val value        = if (properties.nonEmpty) "" else element.getText // for now we don't support both a value and properties
  }

  def extractLocalEvents(actionElement: Element): List[LocalEvent] =
    if (actionElement ne null)
      Dom4j.elements(actionElement, XXFORMS_EVENT_QNAME) map (LocalEvent(_, trusted = false)) toList
    else
      Nil

  def extractServerEventsElements(rootElement: Element) =
    Dom4j.elements(rootElement, XXFORMS_SERVER_EVENTS_QNAME) toList

  // Entry point called by the server: process a sequence of incoming client events.
  def processEvents(
    doc                  : XFormsContainingDocument,
    clientEvents         : List[LocalEvent],
    serverEventsElements : List[Element]
  ): (Boolean, ju.Set[String], Option[String]) = {

    val allClientAndServerEvents = {

      // Process events for noscript mode if needed
      val clientEventsAfterNoscript =
        if (doc.noscript)
          reorderNoscriptEvents(clientEvents, doc)
        else
          clientEvents

      // Decode encrypted server events
      def decodeServerEvents(text: String) =
        Dom4j.elements(decodeXML(text).getRootElement, XXFORMS_EVENT_QNAME) map
          (LocalEvent(_, trusted = true)) toList

      // All global server events
      val globalServerEvents = serverEventsElements flatMap (e ⇒ decodeServerEvents(e.getStringValue))

      // Gather all events including decoding action server events
      globalServerEvents ++
        (clientEventsAfterNoscript flatMap {
          case event if event.name == XXFORMS_SERVER_EVENTS ⇒
            decodeServerEvents(event.value)
          case event ⇒
            List(event)
        })
    }

    if (allClientAndServerEvents.nonEmpty) {

      def filterEvents(events: List[LocalEvent]) = events filter {
        case a if a.name == XXFORMS_ALL_EVENTS_REQUIRED ⇒ false
        case a if (a.name eq null) || (a.targetEffectiveId eq null) ⇒
          debug("ignoring invalid client event", Seq(
            "control id" → a.targetEffectiveId,
            "event name" → a.name)
          )(doc.indentedLogger)
          false
        case _ ⇒ true
      }

      def combineValueEvents(events: List[LocalEvent]): List[XFormsEvent] = events match {
        case Nil              ⇒ Nil
        case List(localEvent) ⇒ safelyCreateAndMapEvent(doc, localEvent).toList
        case _                ⇒

          // Grouping key for value change events
          case class EventGroupingKey(name: String, targetId: String) {
            def this(localEvent: LocalEvent) =
              this(localEvent.name, localEvent.targetEffectiveId)
          }

          // Slide over the events so we can filter and compress them
          // NOTE: Don't use Iterator.toSeq as that returns a Stream, which evaluates lazily. This would be great, except
          // that we *must* first create all events, then dispatch them, so that references to XFormsTarget are obtained
          // beforehand.
          (events ++ DummyEvent).sliding(2).toList flatMap {
            case List(a, b) ⇒
              if (a.name != XXFORMS_VALUE || new EventGroupingKey(a) != new EventGroupingKey(b))
                safelyCreateAndMapEvent(doc, a)
              else
                None
          }
      }

      // Combine and process events
      for (event ← combineValueEvents(filterEvents(allClientAndServerEvents)))
        processEvent(doc, event)

      // Gather some metadata about the events received to help with the response to the client

      // Whether we got a request for all events
      val gotAllEvents = allClientAndServerEvents exists
        (_.name == XXFORMS_ALL_EVENTS_REQUIRED)

      // Set of all control ids for which we got value events
      val valueChangeControlIds = allClientAndServerEvents collect
        { case e if e.name == XXFORMS_VALUE ⇒ e.targetEffectiveId } toSet

      // Last focus/blur event received from the client
      // This ignores server events, see: https://github.com/orbeon/orbeon-forms/issues/2567
      val clientFocusControlId = allClientAndServerEvents.reverse filterNot (_.trusted) collectFirst {
        case e if e.name == XFORMS_FOCUS ⇒ Some(e.targetEffectiveId)
        case e if e.name == XXFORMS_BLUR ⇒ None
      }

      (gotAllEvents, valueChangeControlIds.asJava, clientFocusControlId.orNull)

    } else
      (false, ju.Collections.emptySet[String], null)
  }

  // NOTE: Leave public for unit tests
  def reorderNoscriptEvents(eventElements: List[LocalEvent], doc: XFormsContainingDocument): List[LocalEvent] = {
{'fraction_non_alphanumeric': 0.07983921269665258, 'fraction_numerical': 0.0026335851410354146, 'mean_word_length': 3.045416316232128, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 1, 'lorem ipsum': 0, 'www.': 2, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '827106', 'n_tokens_mistral': 4049, 'n_tokens_neox': 3980, 'n_words': 1231}