Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/main/java/com/zaxxer/nuprocess/NuProcessBuilder.java
### 代码内容：
package com.zaxxer.nuprocess;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

/**
 * This class is used to create operating system processes.
 *

 * Each {@code NuProcessBuilder} instance manages a collection of process
 * attributes. The {@link #start()} method creates a new {@link NuProcess}
 * instance with those attributes. The {@link #start()} method can be invoked
 * repeatedly from the same instance to create new subprocesses with identical
 * or related attributes.
 *

 * Each {@code NuProcessBuilder} manages these attributes:
 *

 *
a command, a list of strings which signifies the external
 * program file to be invoked and its arguments, if any. Which string lists
 * represent a valid operating system command is system-dependent. For example,
 * it is common for each conceptual argument to be an element in this list, but
 * there are operating systems where programs are expected to tokenize command
 * line strings themselves - on such a system a Java implementation might
 * require commands to contain exactly two elements.

 *
 *
an environment, which is a system-dependent mapping from
 * variables to values. The initial value is a copy of the environment of the
 * current process. See {@link System#getenv()}.

 *

 *

 * Modifying a process builder's attributes will affect processes subsequently
 * started by that object's {@link #start()} method, but will never affect
 * previously started processes or the Java process itself.
 *
 * @author Brett Wooldridge
 */
public class NuProcessBuilder
{
   private static final NuProcessFactory factory;

   private final List<String> command;
   private final TreeMap<String, String> environment;
   private Path cwd;
   private NuProcessHandler processListener;

   static {
      String factoryClassName = null;
      String osname = System.getProperty("os.name").toLowerCase();
      if (osname.contains("mac") || osname.contains("freebsd")) {
         factoryClassName = "com.zaxxer.nuprocess.osx.OsxProcessFactory";
      }
      else if (osname.contains("win")) {
         factoryClassName = "com.zaxxer.nuprocess.windows.WinProcessFactory";
      }
      else if (osname.contains("linux")) {
         factoryClassName = "com.zaxxer.nuprocess.linux.LinProcessFactory";
      }
      else if (osname.contains("sunos")) {
         factoryClassName = "com.zaxxer.nuprocess.solaris.SolProcessFactory";
      }

      if (factoryClassName == null) {
         throw new RuntimeException("Unsupported operating system: " + osname);
      }

      try {
         Class<?> forName = Class.forName(factoryClassName);
         factory = (NuProcessFactory) forName.newInstance();
      }
      catch (Exception e) {
         throw new RuntimeException(e);
      }
   }

   /**
    * Constructs a process builder with the specified operating system program
    * and arguments. This constructor makes a copy of the command list. Invokers
    * of this constructor must later call
    * {@link #setProcessListener(NuProcessHandler)} in order to set a
    * {@link NuProcessHandler} instance.
    *
    * @param commands a {@link List} of commands
    * @param environment The environment for the process
    */
   public NuProcessBuilder(List<String> commands, Map<String, String> environment)
   {
      if (commands == null || commands.isEmpty()) {
         throw new IllegalArgumentException("List of commands may not be null or empty");
      }

      this.environment = new TreeMap<String, String>(environment);
      this.command = new ArrayList<String>(commands);
   }

   /**
    * Constructs a process builder with the specified operating system program
    * and arguments. This constructor makes a copy of the command list. Invokers
    * of this constructor must later call
    * {@link #setProcessListener(NuProcessHandler)} in order to set a
    * {@link NuProcessHandler} instance.
    *
    * @param commands a {@link List} of commands
    */
   public NuProcessBuilder(List<String> commands)
   {
      if (commands == null || commands.isEmpty()) {
         throw new IllegalArgumentException("List of commands may not be null or empty");
      }

      this.environment = new TreeMap<String, String>(System.getenv());
      this.command = new ArrayList<String>(commands);
   }

   /**
    * Constructs a process builder with the specified operating system program
    * and arguments. Invokers of this constructor must later call
    * {@link #setProcessListener(NuProcessHandler)} in order to set a
    * {@link NuProcessHandler} instance.
    *
    * @param commands a list of commands
    */
   public NuProcessBuilder(String... commands)
   {
      if (commands == null || commands.length == 0) {
         throw new IllegalArgumentException("List of commands may not be null or empty");
      }

      this.environment = new TreeMap<String, String>(System.getenv());
      this.command = new ArrayList<String>(Arrays.asList(commands));
   }

   /**
    * Constructs a process builder with the specified {@link NuProcessHandler}
    * and operating system program and arguments.
    *
    * @param nuProcessHandler a {@link NuProcessHandler} instance
    * @param commands a list of commands
    */
   public NuProcessBuilder(NuProcessHandler nuProcessHandler, String... commands)
   {
      this(commands);

      if (nuProcessHandler == null) {
         throw new IllegalArgumentException("A NuProcessListener must be specified");
      }

      this.processListener = nuProcessHandler;
   }

   /**
    * Constructs a process builder with the specified {@link NuProcessHandler}
    * and operating system program and arguments. This constructor makes a copy
    * of the command list.
    *
    * @param nuProcessHandler a {@link NuProcessHandler} instance
    * @param commands a {@link List} of commands
    */
   public NuProcessBuilder(NuProcessHandler nuProcessHandler, List<String> commands)
   {
      this(commands);

      if (nuProcessHandler == null) {
         throw new IllegalArgumentException("A NuProcessListener must be specified");
      }

      this.processListener = nuProcessHandler;
   }

   /**
    * Get the {@link List} of commands that were used to construct this
    * {@link NuProcessBuilder}.
    *
    * @return a {@link List} of commands
    */
   public List<String> command()
   {
      return command;
   }

   /**
    * Returns a string map view of this process builder's environment. Whenever
    * a process builder is created, the environment is initialized to a copy of
    * the current process environment. Subprocesses subsequently started by this
    * object's {@link #start()} method will use this map as their environment.
    *

    * The returned object may be modified using ordinary Map operations prior to
    * invoking the {@link #start()} method. The returned map is typically
    * case-sensitive on all platforms.
    *
    * @return This process builder's environment
    */
   public Map<String, String> environment()
   {
      return environment;
   }

   /**
    * Set the {@link NuProcessHandler} instance that will be used for the next
    * and subsequent launch of a {@link NuProcess} when calling the
    * {@link #start()} method.
    *
    * @param listener a {@link NuProcessHandler} instance
    */
   public void setProcessListener(NuProcessHandler listener)
   {
      if (listener == null) {
         throw new IllegalArgumentException("A NuProcessListener must be specified");
      }

      this.processListener = listener;
   }

   /**
    * Set the {@link Path} to which the current working directory (cwd) of the
    * subsequent launch of a {@link NuProcess} will be set when calling the
    * {@link #start()} method.
    *
    * @param cwd a {@link Path} to use for the process's current working
    *        directory, or {@code null} to disable setting the cwd of
    *        subsequently launched proceses
    */
   public void setCwd(Path cwd)
   {
      this.cwd = cwd;
   }

   /**
    * Spawn the child process with the configured commands, environment, and
    * {@link NuProcessHandler}.
    *
    * @return a {@link NuProcess} instance or {@code null} if there is an
    *         immediately detectable launch failure
    */
   public NuProcess start()
   {
      ensureNoNullCharacters(command);
      ensureListener();
      String[] env = prepareEnvironment();

      return factory.createProcess(command, env, processListener, cwd);
   }

   /**
    * Spawn the child process with the configured commands, environment, and {@link NuProcessHandler}
    * and wait for it to complete running.
    *
    * @since 1.3
    */
   public void run()
   {
      ensureNoNullCharacters(command);
      ensureListener();
      String[] env = prepareEnvironment();

      factory.runProcess(command, env, processListener, cwd);
   }

   private void ensureListener()
   {
      if (processListener == null) {
         throw new IllegalArgumentException("NuProcessHandler not specified");
      }
   }

   private void ensureNoNullCharacters(List<String> commands) {
      for (String command : commands) {
         if (command.indexOf('\u0000') >= 0) {
            throw new IllegalArgumentException("Commands may not contain null characters");
         }
      }
   }

   private String[] prepareEnvironment()
   {
      String[] env = new String[environment.size()];
      int i = 0;
      for (Entry<String, String> entrySet : environment.entrySet()) {
         env[i++] = entrySet.getKey() + "=" + entrySet.getValue();
      }

      return env;
   }
}

Output:
【漏洞类型】命令注入漏洞
【严重性】High
【漏洞摘要】原始代码在启动外部进程时，没有对命令字符串中的NUL字符进行检查。NUL字符在某些操作系统中用于表示字符串的结束，如果命令字符串中包含NUL字符，可能会导致命令被截断或在预期之外的地方结束，从而引发命令注入攻击。漏洞的根本原因在于缺少对命令参数中潜在恶意字符的验证。
【关键代码模式】`start` 和 `run` 方法，`ensureNoNullCharacters` 函数，`command` 列表。
【修复方法说明】在 `start` 和 `run` 方法中添加了对 `command` 列表中NUL字符的检查，以确保命令参数的完整性和安全性。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
inlong-manager/manager-pojo/pom.xml
### 代码内容：
<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one or more
  ~ contributor license agreements. See the NOTICE file distributed with
  ~ this work for additional information regarding copyright ownership.
  ~ The ASF licenses this file to You under the Apache License, Version 2.0
  ~ (the "License"); you may not use this file except in compliance with
  ~ the License. You may obtain a copy of the License at
  ~
  ~ http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://maven.apache.org/POM/4.0.0"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.apache.inlong</groupId>
        <artifactId>inlong-manager</artifactId>
        <version>1.4.0-SNAPSHOT</version>
    </parent>

    <artifactId>manager-pojo</artifactId>
    <name>Apache InLong - Manager Pojo</name>

    <dependencies>
        <dependency>
            <groupId>org.apache.inlong</groupId>
            <artifactId>manager-common</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.inlong</groupId>
            <artifactId>sort-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

## 内容2
### 文件路径：
inlong-manager/manager-pojo/src/main/java/org/apache/inlong/manager/pojo/sink/mysql/MySQLSinkDTO.java
### 代码内容：
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.inlong.manager.pojo.sink.mysql;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.inlong.manager.common.enums.ErrorCodeEnum;
import org.apache.inlong.manager.common.exceptions.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;

/**
 * MySQL sink info
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MySQLSinkDTO {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final Logger LOGGER = LoggerFactory.getLogger(MySQLSinkDTO.class);

    @ApiModelProperty("MySQL JDBC URL, such as jdbc:mysql://host:port/database")
    private String jdbcUrl;

    @ApiModelProperty("Username for JDBC URL")
    private String username;

    @ApiModelProperty("User password")
    private String password;

    @ApiModelProperty("Target table name")
    private String tableName;

    @ApiModelProperty("Primary key")
    private String primaryKey;

    @ApiModelProperty("Properties for MySQL")
    private Map<String, Object> properties;

    /**
     * Get the dto instance from the request
     *
     * @param request MySQLSinkRequest
     * @return {@link MySQLSinkDTO}
     */
    public static MySQLSinkDTO getFromRequest(MySQLSinkRequest request) {
        return MySQLSinkDTO.builder()
                .jdbcUrl(request.getJdbcUrl())
                .username(request.getUsername())
                .password(request.getPassword())
                .primaryKey(request.getPrimaryKey())
                .tableName(request.getTableName())
                .properties(request.getProperties())
                .build();
    }

    /**
     * Get MySQL sink info from JSON string
     *
     * @param extParams string ext params
     * @return {@link MySQLSinkDTO}
     */
    public static MySQLSinkDTO getFromJson(@NotNull String extParams) {
        try {
            OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            return OBJECT_MAPPER.readValue(extParams, MySQLSinkDTO.class);
        } catch (Exception e) {
            LOGGER.error("fetch mysql sink info failed from json params: " + extParams, e);
            throw new BusinessException(ErrorCodeEnum.SINK_INFO_INCORRECT.getMessage() + ": " + e.getMessage());
        }
    }

    /**
     * Get MySQL table info
     *
     * @param mySQLSink MySQL sink dto,{@link MySQLSinkDTO}
     * @param columnList MySQL column info list,{@link MySQLColumnInfo}
     * @return {@link MySQLTableInfo}
     */
    public static MySQLTableInfo getTableInfo(MySQLSinkDTO mySQLSink, List<MySQLColumnInfo> columnList) {
        MySQLTableInfo tableInfo = new MySQLTableInfo();
        String dbName = getDbNameFromUrl(mySQLSink.getJdbcUrl());
        tableInfo.setDbName(dbName);
        tableInfo.setTableName(mySQLSink.getTableName());
        tableInfo.setPrimaryKey(mySQLSink.getPrimaryKey());
        tableInfo.setColumns(columnList);
        return tableInfo;
    }

    /**
     * Get DbName from jdbcUrl
     *
     * @param jdbcUrl  MySQL JDBC url, such as jdbc:mysql://host:port/database
     * @return database name
     */
    public static String getDbNameFromUrl(String jdbcUrl) {
        String database = null;

        if (Strings.isNullOrEmpty(jdbcUrl)) {
            throw new IllegalArgumentException("Invalid JDBC url.");
        }

        jdbcUrl = jdbcUrl.toLowerCase();
        if (jdbcUrl.startsWith("jdbc:impala")) {
            jdbcUrl = jdbcUrl.replace(":impala", "");
        }

        int pos1;
        if (!jdbcUrl.startsWith("jdbc:")
                || (pos1 = jdbcUrl.indexOf(':', 5)) == -1) {
            throw new IllegalArgumentException("Invalid JDBC url.");
        }

        String connUri = jdbcUrl.substring(pos1 + 1);
        int pos;
        if (connUri.startsWith("//")) {
            if ((pos = connUri.indexOf('/', 2)) != -1) {
                database = connUri.substring(pos + 1);
            }
        } else {
            database = connUri;
        }

        if (database.contains("?")) {
            database = database.substring(0, database.indexOf("?"));
        }

        if (database.contains(";")) {
            database = database.substring(0, database.indexOf(";"));
        }

        if (Strings.isNullOrEmpty(database)) {
            throw new IllegalArgumentException("Invalid JDBC url.");
        }
        return database;
    }
}


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
packages/vuetify/src/components/VCalendar/mixins/calendar-with-events.ts
### 代码内容：
// Styles
import './calendar-with-events.sass'

// Types
import { VNode, VNodeData } from 'vue'

// Directives
import ripple from '../../../directives/ripple'

// Mixins
import CalendarBase from './calendar-base'

// Helpers
import { escapeHTML } from '../../../util/helpers'

// Util
import props from '../util/props'
import {
  CalendarEventOverlapModes,
} from '../modes'
import {
  getDayIdentifier, diffMinutes,
} from '../util/timestamp'
import {
  parseEvent,
  isEventStart,
  isEventOn,
  isEventOverlapping,
  isEventHiddenOn,
} from '../util/events'
import {
  CalendarTimestamp,
  CalendarEventParsed,
  CalendarEventVisual,
  CalendarEventColorFunction,
  CalendarEventNameFunction,
  CalendarEventTimedFunction,
  CalendarDaySlotScope,
  CalendarDayBodySlotScope,
  CalendarEventOverlapMode,
  CalendarEvent,
  CalendarEventCategoryFunction,
  CalendarCategory,
} from 'vuetify/types'

// Types
type VEventGetter<D> = (day: D) => CalendarEventParsed[]

type VEventVisualToNode<D> = (visual: CalendarEventVisual, day: D) => VNode | false

type VEventsToNodes = <D extends CalendarDaySlotScope>(
  day: D,
  getter: VEventGetter<D>,
  mapper: VEventVisualToNode<D>,
  timed: boolean) => VNode[] | undefined

type VDailyEventsMap = {
  [date: string]: {
    parent: HTMLElement
    more: HTMLElement | null
    events: HTMLElement[]
  }
}

interface VEventScopeInput {
  eventParsed: CalendarEventParsed
  day: CalendarDaySlotScope
  start: boolean
  end: boolean
  timed: boolean
}

const WIDTH_FULL = 100
const WIDTH_START = 95
const MINUTES_IN_DAY = 1440

/* @vue/component */
export default CalendarBase.extend({
  name: 'calendar-with-events',

  directives: {
    ripple,
  },

  props: {
    ...props.events,
    ...props.calendar,
    ...props.category,
  },

  computed: {
    noEvents (): boolean {
      return this.events.length === 0
    },
    parsedEvents (): CalendarEventParsed[] {
      return this.events.map(this.parseEvent)
    },
    parsedEventOverlapThreshold (): number {
      return parseInt(this.eventOverlapThreshold)
    },
    eventTimedFunction (): CalendarEventTimedFunction {
      return typeof this.eventTimed === 'function'
        ? this.eventTimed
        : event => !!event[this.eventTimed as string]
    },
    eventCategoryFunction (): CalendarEventCategoryFunction {
      return typeof this.eventCategory === 'function'
        ? this.eventCategory
        : event => event[this.eventCategory as string]
    },
    eventTextColorFunction (): CalendarEventColorFunction {
      return typeof this.eventTextColor === 'function'
        ? this.eventTextColor
        : () => this.eventTextColor as string
    },
    eventNameFunction (): CalendarEventNameFunction {
      return typeof this.eventName === 'function'
        ? this.eventName
        : (event, timedEvent) => escapeHTML(event.input[this.eventName as string] as string || '')
    },
    eventModeFunction (): CalendarEventOverlapMode {
      return typeof this.eventOverlapMode === 'function'
        ? this.eventOverlapMode
        : CalendarEventOverlapModes[this.eventOverlapMode]
    },
    eventWeekdays (): number[] {
      return this.parsedWeekdays
    },
    categoryMode (): boolean {
      return this.type === 'category'
    },
  },

  methods: {
    eventColorFunction (e: CalendarEvent): string {
      return typeof this.eventColor === 'function'
        ? this.eventColor(e)
        : e.color || this.eventColor
    },
    parseEvent (input: CalendarEvent, index = 0): CalendarEventParsed {
      return parseEvent(
        input,
        index,
        this.eventStart,
        this.eventEnd,
        this.eventTimedFunction(input),
        this.categoryMode ? this.eventCategoryFunction(input) : false,
      )
    },
    formatTime (withTime: CalendarTimestamp, ampm: boolean): string {
      const formatter = this.getFormatter({
        timeZone: 'UTC',
        hour: 'numeric',
        minute: withTime.minute > 0 ? 'numeric' : undefined,
      })

      return formatter(withTime, true)
    },
    updateEventVisibility () {
      if (this.noEvents || !this.eventMore) {
        return
      }

      const eventHeight = this.eventHeight
      const eventsMap = this.getEventsMap()

      for (const date in eventsMap) {
        const { parent, events, more } = eventsMap[date]
        if (!more) {
          break
        }

        const parentBounds = parent.getBoundingClientRect()
        const last = events.length - 1
        const eventsSorted = events.map(event => ({
          event,
          bottom: event.getBoundingClientRect().bottom,
        })).sort((a, b) => a.bottom - b.bottom)
        let hidden = 0

        for (let i = 0; i <= last; i++) {
          const bottom = eventsSorted[i].bottom
          const hide = i === last
            ? (bottom > parentBounds.bottom)
            : (bottom + eventHeight > parentBounds.bottom)

          if (hide) {
            eventsSorted[i].event.style.display = 'none'
            hidden++
          }
        }

        if (hidden) {
          more.style.display = ''
          more.innerHTML = this.$vuetify.lang.t(this.eventMoreText, hidden)
        } else {
          more.style.display = 'none'
        }
      }
    },
    getEventsMap (): VDailyEventsMap {
      const eventsMap: VDailyEventsMap = {}
      const elements = this.$refs.events as HTMLElement[]

      if (!elements || !elements.forEach) {
        return eventsMap
      }

      elements.forEach(el => {
        const date = el.getAttribute('data-date')
        if (el.parentElement && date) {
          if (!(date in eventsMap)) {
            eventsMap[date] = {
              parent: el.parentElement,
              more: null,
              events: [],
            }
          }
          if (el.getAttribute('data-more')) {
            eventsMap[date].more = el
          } else {
            eventsMap[date].events.push(el)
            el.style.display = ''
          }
        }
      })

      return eventsMap
    },
    genDayEvent ({ event }: CalendarEventVisual, day: CalendarDaySlotScope): VNode {
      const eventHeight = this.eventHeight
      const eventMarginBottom = this.eventMarginBottom
      const dayIdentifier = getDayIdentifier(day)
      const week = day.week
      const start = dayIdentifier === event.startIdentifier
      let end = dayIdentifier === event.endIdentifier
      let width = WIDTH_START

      if (!this.categoryMode) {
        for (let i = day.index + 1; i < week.length; i++) {
          const weekdayIdentifier = getDayIdentifier(week[i])
          if (event.endIdentifier >= weekdayIdentifier) {
            width += WIDTH_FULL
            end = end || weekdayIdentifier === event.endIdentifier
          } else {
            end = true
            break
          }
        }
      }
      const scope = { eventParsed: event, day, start, end, timed: false }

      return this.genEvent(event, scope, false, {
        staticClass: 'v-event',
        class: {
          'v-event-start': start,
          'v-event-end': end,
        },
        style: {
          height: `${eventHeight}px`,
          width: `${width}%`,
          'margin-bottom': `${eventMarginBottom}px`,
        },
        attrs: {
          'data-date': day.date,
        },
        key: event.index,
        ref: 'events',
        refInFor: true,
      })
    },
    genTimedEvent ({ event, left, width }: CalendarEventVisual, day: CalendarDayBodySlotScope): VNode | false {
      if (day.timeDelta(event.end) < 0 || day.timeDelta(event.start) >= 1 || isEventHiddenOn(event, day)) {
        return false
      }

      const dayIdentifier = getDayIdentifier(day)
      const start = event.startIdentifier >= dayIdentifier
      const end = event.endIdentifier > dayIdentifier
      const top = start ? day.timeToY(event.start) : 0
      const bottom = end ? day.timeToY(MINUTES_IN_DAY) : day.timeToY(event.end)
      const height = Math.max(this.eventHeight, bottom - top)
      const scope = { eventParsed: event, day, start, end, timed: true }

      return this.genEvent(event, scope, true, {
        staticClass: 'v-event-timed',
        style: {
          top: `${top}px`,
          height: `${height}px`,
          left: `${left}%`,
          width: `${width}%`,
        },
      })
    },
    genEvent (event: CalendarEventParsed, scopeInput: VEventScopeInput, timedEvent: boolean, data: VNodeData): VNode {
      const slot = this.$scopedSlots.event
      const text = this.eventTextColorFunction(event.input)
      const background = this.eventColorFunction(event.input)
      const overlapsNoon = event.start.hour < 12 && event.end.hour >= 12
      const singline = diffMinutes(event.start, event.end) <= this.parsedEventOverlapThreshold
      const formatTime = this.formatTime
      const timeSummary = () => formatTime(event.start, overlapsNoon) + ' - ' + formatTime(event.end, true)
      const eventSummary = () => {
        const name = this.eventNameFunction(event, timedEvent)
        if (event.start.hasTime) {
          const eventSummaryClass = 'v-event-summary'
          if (timedEvent) {
            const time = timeSummary()
            const delimiter = singline ? ', ' : '<br>'

            return `<span class="${eventSummaryClass}"><strong>${name}</strong>${delimiter}${time}</span>`
          } else {
            const time = formatTime(event.start, true)

            return `<span class="${eventSummaryClass}"><strong>${time}</strong> ${name}</span>`
          }
        }

        return name
      }

      const scope = {
        ...scopeInput,
        event: event.input,
        outside: scopeInput.day.outside,
        singline,
        overlapsNoon,
        formatTime,
        timeSummary,
        eventSummary,
      }

      return this.$createElement('div',
        this.setTextColor(text,
          this.setBackgroundColor(background, {
            on: this.getDefaultMouseEventHandlers(':event', nativeEvent => ({ ...scope, nativeEvent })),
            directives: [{
              name: 'ripple',
              value: this.eventRipple ?? true,
            }],
            ...data,
          })
        ), slot
          ? slot(scope)
          : [this.genName(eventSummary)]
      )
    },
    genName (eventSummary: () => string): VNode {
      return this.$createElement('div', {
        staticClass: 'pl-1',
        domProps: {
          innerHTML: eventSummary(),
        },
      })
    },
    genPlaceholder (day: CalendarTimestamp): VNode {
      const height = this.eventHeight + this.eventMarginBottom

      return this.$createElement('div', {
        style: {
          height: `${height}px`,
        },
        attrs: {
          'data-date': day.date,
        },
        ref: 'events',
        refInFor: true,
      })
    },
    genMore (day: CalendarDaySlotScope): VNode {
      const eventHeight = this.eventHeight
      const eventMarginBottom = this.eventMarginBottom

      return this.$createElement('div', {
        staticClass: 'v-event-more pl-1',
        class: {
          'v-outside': day.outside,
        },
        attrs: {
          'data-date': day.date,
          'data-more': 1,
        },
        directives: [{
          name: 'ripple',
          value: this.eventRipple ?? true,
        }],
        on: this.getDefaultMouseEventHandlers(':more', nativeEvent => {
          return { nativeEvent, ...day }
        }),

        style: {
          display: 'none',
          height: `${eventHeight}px`,
          'margin-bottom': `${eventMarginBottom}px`,
        },
        ref: 'events',
        refInFor: true,
      })
    },
    getVisibleEvents (): CalendarEventParsed[] {
      const start = getDayIdentifier(this.days[0])
      const end = getDayIdentifier(this.days[this.days.length - 1])

      return this.parsedEvents.filter(
        event => isEventOverlapping(event, start, end)
      )
    },
    isEventForCategory (event: CalendarEventParsed, category: CalendarCategory): boolean {
      return !this.categoryMode ||
        (typeof category === 'object' && category.categoryName &&
        category.categoryName === event.category) ||
        (typeof event.category === 'string' && category === event.category) ||
        (typeof event.category !== 'string' && category === null)
    },
    getEventsForDay (day: CalendarDaySlotScope): CalendarEventParsed[] {
      const identifier = getDayIdentifier(day)
      const firstWeekday = this.eventWeekdays[0]

      return this.parsedEvents.filter(
        event => isEventStart(event, day, identifier, firstWeekday)
      )
    },
    getEventsForDayAll (day: CalendarDaySlotScope): CalendarEventParsed[] {
      const identifier = getDayIdentifier(day)
      const firstWeekday = this.eventWeekdays[0]

      return this.parsedEvents.filter(
        event => event.allDay &&
          (this.categoryMode ? isEventOn(event, identifier) : isEventStart(event, day, identifier, firstWeekday)) &&
          this.isEventForCategory(event, day.category)
      )
    },
    getEventsForDayTimed (day: CalendarDaySlotScope): CalendarEventParsed[] {
      const identifier = getDayIdentifier(day)
      return this.parsedEvents.filter(
        event => !event.allDay &&
          isEventOn(event, identifier) &&
          this.isEventForCategory(event, day.category)
      )
    },
    getScopedSlots () {
      if (this.noEvents) {
        return { ...this.$scopedSlots }
      }

      const mode = this.eventModeFunction(
        this.parsedEvents,
        this.eventWeekdays[0],
        this.parsedEventOverlapThreshold
      )

      const isNode = (input: VNode | false): input is VNode => !!input
      const getSlotChildren: VEventsToNodes = (day, getter, mapper, timed) => {
        const events = getter(day)
        const visuals = mode(day, events, timed, this.categoryMode)

        if (timed) {
          return visuals.map(visual => mapper(visual, day)).filter(isNode)
        }

        const children: VNode[] = []

        visuals.forEach((visual, index) => {
          while (children.length < visual.column) {
            children.push(this.genPlaceholder(day))
          }

          const mapped = mapper(visual, day)
          if (mapped) {
            children.push(mapped)
          }
        })

        return children
      }

      const slots = this.$scopedSlots
      const slotDay = slots.day
      const slotDayHeader = slots['day-header']
      const slotDayBody = slots['day-body']

      return {
        ...slots,
        day: (day: CalendarDaySlotScope) => {
          let children = getSlotChildren(day, this.getEventsForDay, this.genDayEvent, false)
          if (children && children.length > 0 && this.eventMore) {
            children.push(this.genMore(day))
          }
          if (slotDay) {
            const slot = slotDay(day)
            if (slot) {
              children = children ? children.concat(slot) : slot
            }
          }
          return children
        },
        'day-header': (day: CalendarDaySlotScope) => {
          let children = getSlotChildren(day, this.getEventsForDayAll, this.genDayEvent, false)

          if (slotDayHeader) {
            const slot = slotDayHeader(day)
            if (slot) {
              children = children ? children.concat(slot) : slot
            }
          }
          return children
        },
        'day-body': (day: CalendarDayBodySlotScope) => {
          const events = getSlotChildren(day, this.getEventsForDayTimed, this.genTimedEvent, true)
          let children: VNode[] = [
            this.$createElement('div', {
              staticClass: 'v-event-timed-container',
            }, events),
          ]

          if (slotDayBody) {
            const slot = slotDayBody(day)
            if (slot) {
              children = children.concat(slot)
            }
          }
          return children
        },
      }
    },
  },
})

## 内容2
### 文件路径：
packages/vuetify/types/index.d.ts
### 代码内容：
import Vue, { Component, PluginFunction, VueConstructor, DirectiveOptions } from 'vue'
import './lib'
import './alacarte'
import './colors'

// Services
import { Application } from './services/application'
import { Breakpoint } from './services/breakpoint'
import { Icons } from './services/icons'
import { Lang } from './services/lang'
import { Theme } from './services/theme'
import {
  Presets,
  UserVuetifyPreset,
  VuetifyPreset,
} from './services/presets'

// Service Options
import { GoToOptions, VuetifyGoToTarget } from './services/goto'

export default class Vuetify {
  constructor (preset?: Partial<UserVuetifyPreset>)

  static install: PluginFunction<VuetifyUseOptions>
  static version: string
  static config: Config

  framework: Framework
  preset: VuetifyPreset
  userPreset: UserVuetifyPreset
}

export interface Config {
  silent: boolean
}

export { Presets, VuetifyPreset, UserVuetifyPreset } from './services/presets'

export type ComponentOrPack = Component & {
  $_vuetify_subcomponents?: Record<string, ComponentOrPack>
}

export interface VuetifyUseOptions {
  transitions?: Record<string, VueConstructor>
  directives?: Record<string, DirectiveOptions>
  components?: Record<string, ComponentOrPack>
}

export interface Framework {
  readonly breakpoint: Breakpoint
  readonly goTo: (target: VuetifyGoToTarget, options?: GoToOptions) => Promise<number>
  application: Application
  theme: Theme
  icons: Icons
  lang: Lang
  presets: Presets
  rtl: boolean
}

declare module 'vue/types/vue' {
  export interface Vue {
    $vuetify: Framework
  }
}

declare module 'vue/types/options' {
  export interface ComponentOptions<
    V extends Vue,
    Data=DefaultData<V>,
    Methods=DefaultMethods<V>,
    Computed=DefaultComputed,
    PropsDef=PropsDefinition<DefaultProps>,
    Props=DefaultProps> {
    vuetify?: Vuetify
  }
}

// Public types
export type TreeviewItemFunction = (item: object, search: string, textKey: string) => boolean

export type SelectItemKey = string | (string | number)[] | ((item: object, fallback?: any) => any)

export interface ItemGroup<T> {
  name: string
  items: T[]
}

export interface DataOptions {
  page: number
  itemsPerPage: number
  sortBy: string[]
  sortDesc: boolean[]
  groupBy: string[]
  groupDesc: boolean[]
  multiSort: boolean
  mustSort: boolean
}

export interface DataPagination {
  page: number
  itemsPerPage: number
  pageStart: number
  pageStop: number
  pageCount: number
  itemsLength: number
}

export interface DataItemProps {
  index: number
  item: any
  select: (v: boolean) => void
  isSelected: boolean
  expand: (v: boolean) => void
  isExpanded: boolean
  isMobile: boolean
}

export interface DataTableItemProps extends DataItemProps {
  headers: DataTableHeader[]
}

export interface DataScopeProps {
  originalItemsLength: number
  items: any[]
  pagination: DataPagination
  options: DataOptions
  updateOptions: (obj: any) => void
  sort: (value: string) => void
  sortArray: (sortBy: string[]) => void
  group: (value: string) => void
  groupedItems: ItemGroup<any>[] | null
}

export type DataTableCompareFunction<T = any> = (a: T, b: T) => number

export type DataSortFunction<T extends any = any> = (
  items: T[],
  sortBy: string[],
  sortDesc: boolean[],
  locale: string,
  customSorters?: Record<string, DataTableCompareFunction<T>>
) => T[];

export type DataGroupFunction<T extends any = any> = (
  items: T[],
  groupBy: string[],
  groupDesc: boolean[],
) => ItemGroup<T>[]

export type DataSearchFunction<T extends any = any> = (items: T[], search: string) => T[]

export type DatePickerFormatter = (date: string) => string

export type DatePickerAllowedDatesFunction = (date: string) => boolean

export type DatePickerEventColorValue = string | string[]

export type DatePickerEvents = string[] | ((date: string) => boolean | DatePickerEventColorValue) | Record<string, DatePickerEventColorValue>

export type DatePickerEventColors = DatePickerEventColorValue | Record<string, DatePickerEventColorValue> | ((date: string) => DatePickerEventColorValue)

export type DatePickerType = 'date' | 'month'

export type DatePickerMultipleFormatter = (date: string[]) => string

export interface TouchHandlers {
  start?: (wrapperEvent: TouchEvent & TouchWrapper) => void
  end?: (wrapperEvent: TouchEvent & TouchWrapper) => void
  move?: (wrapperEvent: TouchEvent & TouchWrapper) => void
  left?: (wrapper: TouchWrapper) => void
  right?: (wrapper: TouchWrapper) => void
  up?: (wrapper: TouchWrapper) => void
  down?: (wrapper: TouchWrapper) => void
}

export interface TouchWrapper extends TouchHandlers {
  touchstartX: number
  touchstartY: number
  touchmoveX: number
  touchmoveY: number
  touchendX: number
  touchendY: number
  offsetX: number
  offsetY: number
}

export type TouchValue = TouchHandlers & {
  parent?: boolean
  options?: AddEventListenerOptions
}

export type InputValidationRule = (value: any) => string | boolean

export type InputMessage = string | string[]

export type InputValidationRules = (InputValidationRule | string)[]

export type CalendarCategory =
  | string
  | {
      name?: string
      categoryName?: string
      [key: string]: any
    }

export type CalendarCategoryTextFunction = (
  category: CalendarCategory
) => string

export interface CalendarTimestamp {
  date: string
  time: string
  year: number
  month: number
  day: number
  weekday: number
  hour: number
  minute: number
  hasDay: boolean
  hasTime: boolean
  past: boolean
  present: boolean
  future: boolean
  category?: CalendarCategory
}

export type CalendarFormatter = (timestamp: CalendarTimestamp, short: boolean) => string

export interface CalendarEvent {
  [prop: string]: any
}

export interface CalendarEventParsed {
  input: CalendarEvent
  start: CalendarTimestamp
  startIdentifier: number
  startTimestampIdentifier: number
  end: CalendarTimestamp
  endIdentifier: number
  endTimestampIdentifier: number
  allDay: boolean
  index: number
  category: string | false
}

export interface CalendarEventVisual {
  event: CalendarEventParsed
  columnCount: number
  column: number
  left: number
  width: number
}

export interface CalendarDaySlotScope extends CalendarTimestamp {
  outside: boolean
  index: number
  week: CalendarTimestamp[]
  category: CalendarCategory
}

export type CalendarTimeToY = (time: CalendarTimestamp | number | string, clamp?: boolean) => number

export type CalendarTimeDelta = (time: CalendarTimestamp | number | string) => number | false

export interface CalendarDayBodySlotScope extends CalendarDaySlotScope {
  timeToY: CalendarTimeToY
  timeDelta: CalendarTimeDelta
}

export type CalendarEventOverlapMode = (events: CalendarEventParsed[], firstWeekday: number, overlapThreshold: number) => (day: CalendarDaySlotScope, dayEvents: CalendarEventParsed[], timed: boolean, reset: boolean) => CalendarEventVisual[]

export type CalendarEventColorFunction = (event: CalendarEvent) => string

export type CalendarEventTimedFunction = (event: CalendarEvent) => boolean

export type CalendarEventCategoryFunction = (event: CalendarEvent) => string

export type CalendarEventNameFunction = (event: CalendarEventParsed, timedEvent: boolean) => string

export type DataTableFilterFunction = (value: any, search: string | null, item: any) => boolean

export interface DataTableHeader<T extends any = any> {
  text: string
  value: string
  align?: 'start' | 'center' | 'end'
  sortable?: boolean
  filterable?: boolean
  groupable?: boolean
  divider?: boolean
  class?: string | string[]
  cellClass?: string | string[]
  width?: string | number
  filter?: (value: any, search: string | null, item: any) => boolean
  sort?: DataTableCompareFunction<T>
}

export type DataItemsPerPageOption = (number | {
  text: string
  value: number
});

export type RowClassFunction = (item: any) => null | undefined | string | string[] | Record<string, boolean>

Output:
【漏洞类型】跨站脚本攻击（XSS）
【严重性】High
【漏洞摘要】此代码存在跨站脚本攻击（XSS）漏洞。在`eventNameFunction`方法中，当`eventName`属性不是函数时，它直接将`event.input[this.eventName as string]`的内容作为字符串拼接到HTML中，而没有进行任何HTML转义处理。这种做法可能导致恶意用户通过输入包含JavaScript代码的字符串，从而在页面中执行这些代码，从而实现XSS攻击。
【关键代码模式】
1. `eventNameFunction`方法。
2. `escapeHTML`函数的调用。
3. `innerHTML`属性的使用。
【修复方法说明】修复方法通过移除`escapeHTML`函数的调用，并将返回的字符串内容转换为虚拟节点（VNodes），从而避免直接将用户输入插入到HTML中，解决问题。最佳实践是始终对用户输入进行HTML转义处理，或者使用虚拟节点来构建动态HTML内容。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
server-core/src/main/java/io/onedev/server/web/page/project/builds/detail/artifacts/ArtifactUploadPanel.java
### 代码内容：
package io.onedev.server.web.page.project.builds.detail.artifacts;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Callable;

import org.apache.commons.compress.utils.IOUtils;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.util.lang.Bytes;

import io.onedev.commons.utils.FileUtils;
import io.onedev.commons.utils.LockUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.Build;
import io.onedev.server.util.FilenameUtils;
import io.onedev.server.web.component.dropzonefield.DropzoneField;
import io.onedev.server.web.util.FileUpload;

@SuppressWarnings("serial")
public abstract class ArtifactUploadPanel extends Panel {

	private String directory;

	private final Collection<FileUpload> uploads = new ArrayList<>();

	public ArtifactUploadPanel(String id) {
		super(id);
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		int maxUploadFileSize = OneDev.getInstance(SettingManager.class)
				.getPerformanceSetting().getMaxUploadFileSize();

		Form<?> form = new Form<Void>("form");
		form.setMultiPart(true);
		form.setFileMaxSize(Bytes.megabytes(maxUploadFileSize));
		add(form);

		form.add(new AjaxLink<Void>("close") {

			@Override
			public void onClick(AjaxRequestTarget target) {
				onCancel(target);
			}

		});

		FencedFeedbackPanel feedback = new FencedFeedbackPanel("feedback", form);
		feedback.setOutputMarkupPlaceholderTag(true);
		form.add(feedback);

		DropzoneField dropzone = new DropzoneField(
				"files",
				new PropertyModel<Collection<FileUpload>>(this, "uploads"),
				null, 0, maxUploadFileSize);
		dropzone.setRequired(true).setLabel(Model.of("File"));
		form.add(dropzone);

		form.add(new AjaxButton("upload") {

			@Override
			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
				super.onSubmit(target, form);

				LockUtils.write(getBuild().getArtifactsLockKey(), new Callable<Void>() {

					@Override
					public Void call() throws Exception {
						File artifactsDir = getBuild().getArtifactsDir();
						for (FileUpload upload: uploads) {
							String filePath = FilenameUtils.sanitizeFilename(upload.getFileName());
							if (directory != null)
								filePath = directory + "/" + filePath;
							File file = new File(artifactsDir, filePath);
							FileUtils.createDir(file.getParentFile());
							try (	InputStream is = upload.getInputStream();
									OutputStream os = new FileOutputStream(file)) {
								IOUtils.copy(is, os);
							} finally {
								upload.release();
							}
						}
						return null;
					}

				});

				onUploaded(target);
			}

			@Override
			protected void onError(AjaxRequestTarget target, Form<?> form) {
				super.onError(target, form);
				target.add(feedback);
			}

		});

		form.add(new TextField<String>("directory", new PropertyModel<String>(this, "directory")));

		form.add(new AjaxLink<Void>("cancel") {

			@Override
			public void onClick(AjaxRequestTarget target) {
				onCancel(target);
			}

		});
	}

	public abstract void onUploaded(AjaxRequestTarget target);

	public abstract void onCancel(AjaxRequestTarget target);

	protected abstract Build getBuild();

}

Output:
【漏洞类型】路径遍历漏洞

【严重性】高

【漏洞摘要】这是一个路径遍历漏洞。原始代码中，通过用户输入的`directory`字段和上传的文件名来构建文件路径，但没有对`directory`字段进行任何验证。如果攻击者能够控制`directory`字段的内容，他们可以利用路径遍历攻击，将文件上传到服务器上的任意目录。具体表现为在`ArtifactUploadPanel.java`文件的第78行，`filePath`变量的构建没有对`directory`字段进行合法性检查。

【关键代码模式】
- `directory.contains("..")`：这是检查用户输入的`directory`字段是否包含路径遍历攻击字符串的代码。
- `FilenameUtils.sanitizeFilename(upload.getFileName())`：这个函数用于清理文件名，防止文件名中的恶意字符。
- `LockUtils.write(getBuild().getArtifactsLockKey(), new Callable<Void>() {...})`：这个函数用于在上传文件时获取写锁，以确保并发操作的线程安全。

【修复方法说明】修复方法通过在`onSubmit`方法中添加对`directory`字段的检查，确保其不包含路径遍历攻击字符串`".."`，从而防止攻击者将文件上传到服务器上的任意目录。最佳实践是始终对用户输入进行严格的验证和清理，以防止路径遍历和其他类型的攻击。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
server-core/src/main/java/io/onedev/server/web/resource/ArtifactResource.java
### 代码内容：
package io.onedev.server.web.resource;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import javax.persistence.EntityNotFoundException;

import org.apache.shiro.authz.UnauthorizedException;
import org.apache.tika.io.IOUtils;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.AbstractResource;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;

import io.onedev.commons.utils.ExplicitException;
import io.onedev.commons.utils.LockUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.BuildManager;
import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.model.Build;
import io.onedev.server.model.Project;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.ContentDetector;

public class ArtifactResource extends AbstractResource {

	private static final long serialVersionUID = 1L;

	private static final String PARAM_PROJECT = "project";

	private static final String PARAM_BUILD = "build";

	@Override
	protected ResourceResponse newResourceResponse(Attributes attributes) {
		PageParameters params = attributes.getParameters();

		Long projectId = params.get(PARAM_PROJECT).toLong();
		Project project = OneDev.getInstance(ProjectManager.class).load(projectId);

		Long buildNumber = params.get(PARAM_BUILD).toOptionalLong();

		if (buildNumber == null)
			throw new IllegalArgumentException("build number has to be specified");

		Build build = OneDev.getInstance(BuildManager.class).find(project, buildNumber);

		if (build == null) {
			String message = String.format("Unable to find build (project: %s, build number: %d)",
					project.getPath(), buildNumber);
			throw new EntityNotFoundException(message);
		}

		if (!SecurityUtils.canAccess(build))
			throw new UnauthorizedException();

		List<String> pathSegments = new ArrayList<>();

		for (int i = 0; i < params.getIndexedCount(); i++) {
			String pathSegment = params.get(i).toString();
			if (pathSegment.length() != 0)
				pathSegments.add(pathSegment);
		}

		if (pathSegments.isEmpty())
			throw new ExplicitException("Artifact path has to be specified");

		String artifactPath = Joiner.on("/").join(pathSegments);

		File artifactsDir = build.getArtifactsDir();
		File artifactFile = new File(artifactsDir, artifactPath);
		if (!artifactFile.exists() || artifactFile.isDirectory()) {
			String message = String.format("Specified artifact path does not exist or is a directory (project: %s, build number: %d, path: %s)",
					project.getPath(), build.getNumber(), artifactPath);
			throw new ExplicitException(message);
		}

		ResourceResponse response = new ResourceResponse();
		try (InputStream is = new BufferedInputStream(new FileInputStream(artifactFile))) {
			response.setContentType(ContentDetector.detectMediaType(is, artifactPath).toString());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		response.disableCaching();

		try {
			response.setFileName(URLEncoder.encode(artifactFile.getName(), StandardCharsets.UTF_8.name()));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}

		response.setContentLength(artifactFile.length());

		response.setWriteCallback(new WriteCallback() {

			@Override
			public void writeData(Attributes attributes) throws IOException {
				LockUtils.read(build.getArtifactsLockKey(), new Callable<Void>() {

					@Override
					public Void call() throws Exception {
						try (InputStream is = new FileInputStream(artifactFile)) {
							IOUtils.copy(is, attributes.getResponse().getOutputStream());
						}
						return null;
					}

				});
			}

		});

		return response;
	}

	public static PageParameters paramsOf(Project project, Long buildNumber, String path) {
		PageParameters params = new PageParameters();
		params.set(PARAM_PROJECT, project.getId());
		params.set(PARAM_BUILD, buildNumber);

		int index = 0;
		for (String segment: Splitter.on("/").split(path)) {
			params.set(index, segment);
			index++;
		}
		return params;
	}

}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】此代码存在路径遍历漏洞，攻击者可以通过精心构造的输入参数访问服务器上任意文件。
【关键代码模式】
- `params.getIndexedCount()`
- `params.get(i).toString()`
- `new File(artifactsDir, artifactPath)`
【修复方法说明】通过设置响应头 `X-Content-Type-Options: nosniff` 并将内容类型统一设置为 `application/octet-stream`，防止浏览器对文件内容进行类型嗅探，从而减少潜在的XSS攻击。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/main/java/com/diffplug/gradle/ZipMisc.java
### 代码内容：
package com.diffplug.gradle;

import com.diffplug.common.base.Errors;
import com.diffplug.common.base.StringPrinter;
import com.diffplug.common.base.Throwing;
import com.diffplug.common.io.ByteSink;
import com.diffplug.common.io.ByteSource;
import com.diffplug.common.io.ByteStreams;
import com.diffplug.common.io.Files;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.IOUtils;

/** Utilities for mucking with zip files. */
public class ZipMisc {
	/**
	 * Reads the given entry from the zip.
	 *
	 * @param input		a zip file
	 * @param toRead	a path within that zip file
	 * @param reader	will be called with an InputStream containing the contents of that entry in the zip file
	 */
	public static void read(File input, String toRead, Throwing.Specific.Consumer<InputStream, IOException> reader) throws IOException {
		try (
				ZipFile file = new ZipFile(input);
				InputStream stream = file.getInputStream(file.getEntry(toRead));) {
			reader.accept(stream);
		} catch (NullPointerException e) {
			if (e.getMessage().equals("entry")) {
				System.err.println("No such entry: " + toRead);
				try (ZipFile file = new ZipFile(input)) {
					Enumeration<? extends ZipEntry> entries = file.entries();
					while (entries.hasMoreElements()) {
						ZipEntry entry = entries.nextElement();
						System.err.println("  available: " + entry.getName());
					}
				}
			}
		}
	}

	/**
	 * Reads the given entry from the zip.
	 *
	 * @param input		a zip file
	 * @param toRead	a path within that zip file
	 * @return the given path within the zip file decoded as a UTF8 string, with only unix newlines.
	 */
	public static String read(File input, String toRead) throws IOException {
		String raw = StringPrinter.buildString(Errors.rethrow().wrap(printer -> {
			read(input, toRead, inputStream -> {
				copy(inputStream, printer.toOutputStream(StandardCharsets.UTF_8));
			});
		}));
		return FileMisc.toUnixNewline(raw);
	}

	/**
	 * Modifies only the specified entries in a zip file.
	 *
	 * @param input 		a source from a zip file
	 * @param output		an output to a zip file
	 * @param toModify		a map from path to an input stream for the entries you'd like to change
	 * @param toOmit		a set of entries you'd like to leave out of the zip
	 * @throws IOException
	 */
	public static void modify(ByteSource input, ByteSink output, Map<String, Function<byte[], byte[]>> toModify, Predicate<String> toOmit) throws IOException {
		try (ZipInputStream zipInput = new ZipInputStream(input.openBufferedStream());
				ZipOutputStream zipOutput = new ZipOutputStream(output.openBufferedStream())) {
			while (true) {
				// read the next entry
				ZipEntry entry = zipInput.getNextEntry();
				if (entry == null) {
					break;
				}

				Function<byte[], byte[]> replacement = toModify.get(entry.getName());
				if (replacement != null) {
					byte[] clean = ByteStreams.toByteArray(zipInput);
					byte[] modified = replacement.apply(clean);
					// if it's the entry being modified, enter the modified stuff
					try (InputStream replacementStream = new ByteArrayInputStream(modified)) {
						ZipEntry newEntry = new ZipEntry(entry.getName());
						newEntry.setComment(entry.getComment());
						newEntry.setExtra(entry.getExtra());
						newEntry.setMethod(entry.getMethod());
						newEntry.setTime(entry.getTime());

						zipOutput.putNextEntry(newEntry);
						copy(replacementStream, zipOutput);
					}
				} else if (!toOmit.test(entry.getName())) {
					// if it isn't being modified, just copy the file stream straight-up
					ZipEntry newEntry = new ZipEntry(entry);
					newEntry.setCompressedSize(-1);
					zipOutput.putNextEntry(newEntry);
					copy(zipInput, zipOutput);
				}

				// close the entries
				zipInput.closeEntry();
				zipOutput.closeEntry();
			}
		}
	}

	/** Modifies a file in-place. */
	public static void modify(File file, Map<String, Function<byte[], byte[]>> toModify, Predicate<String> toOmit) throws IOException {
		byte[] allContent = Files.asByteSource(file).read();
		ByteSource source = ByteSource.wrap(allContent);
		ByteSink sink = Files.asByteSink(file);
		modify(source, sink, toModify, toOmit);
	}

	/**
	 * Creates a single-entry zip file.
	 *
	 * @param input					an uncompressed file
	 * @param pathWithinArchive		the path within the archive
	 * @param output				the new zip file it will be compressed into
	 */
	public static void zip(File input, String pathWithinArchive, File output) throws IOException {
		try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(output)))) {
			zipStream.setMethod(ZipOutputStream.DEFLATED);
			zipStream.setLevel(9);
			zipStream.putNextEntry(new ZipEntry(pathWithinArchive));
			try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(input))) {
				copy(inputStream, zipStream);
			}
		}
	}

	/** Copies one stream into the other. */
	private static void copy(InputStream input, OutputStream output) throws IOException {
		IOUtils.copy(input, output);
	}

	/**
	 * Unzips a directory to a folder.
	 *
	 * @param input				a zip file
	 * @param destinationDir	where the zip will be extracted to
	 */
	public static void unzip(File input, File destinationDir) throws IOException {
		try (ZipInputStream zipInput = new ZipInputStream(new BufferedInputStream(new FileInputStream(input)))) {
			ZipEntry entry;
			while ((entry = zipInput.getNextEntry()) != null) {
				File dest = new File(destinationDir, entry.getName());
				if (!dest.toPath().normalize().startsWith(destinationDir.toPath().normalize())) {
					throw new RuntimeException("Bad zip entry");
				}
				if (entry.isDirectory()) {
					FileMisc.mkdirs(dest);
				} else {
					FileMisc.mkdirs(dest.getParentFile());
					try (OutputStream output = new BufferedOutputStream(new FileOutputStream(dest))) {
						copy(zipInput, output);
					}
				}
			}
		}
	}
}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】此代码存在路径遍历漏洞，具体表现为在解压文件时，未对目标文件路径进行正确的验证。在 `unzip` 方法中，`entry.getName()` 可能包含相对路径或尝试跳出目标目录的路径（例如 `../../maliciousFile.txt`），这会导致文件被解压到预期之外的位置，从而可能引发安全问题。问题行或模式是：`File dest = new File(destinationDir, entry.getName());`
【关键代码模式】与路径遍历漏洞相关的关键代码模式包括：
- `File dest = new File(destinationDir, entry.getName());`
- `ZipEntry entry = zipInput.getNextEntry();`
- `entry.isDirectory()`
- `entry.getName()`
- 相关的输入输出流操作，如 `copy(zipInput, output);`
【修复方法说明】修复方法通过在创建目标文件路径时进行规范化路径检查，防止路径遍历攻击。具体通过 `if (!dest.toPath().normalize().startsWith(destinationDir.toPath().normalize()))` 验证目标文件路径是否在预期的目录下。这种方法解决了路径验证的问题，确保了只在目标目录内进行文件解压。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-tag/xwiki-platform-tag-ui/src/main/resources/Main/Tags.xml
### 代码内容：
<?xml version="1.1" encoding="UTF-8"?>

<xwikidoc version="1.2" reference="Main.Tags" locale="">
  <web>Main</web>
  <name>Tags</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>Main.WebHome</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>$services.localization.render("xe.tag.tags")</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.1</syntaxId>
  <hidden>true</hidden>
  <content>{{velocity}}
##
## Tag application.
##
## - See a Tag Cloud of all tags within the wiki.
## - See all the pages tagged with the given tag.
## - Rename a tag.
## - Delete a tag.
##
$xwiki.ssx.use('Main.Tags')##
##
## Set application variables (action and tag) from URL parameters.
##
#set ($do = "$!{request.get('do')}")
#set ($tag = "$!{request.get('tag')}")
#set ($urlEscapedTag = $escapetool.url($tag))
#set ($htmlEscapedTag = $escapetool.xml($tag))
##
## Macro displayTagAppTitle. Display level1 title of this app.
##
#macro (displayTagAppTitle $urlEscapedTag $htmlEscapedTag $displayButtons)
  (% class="xapp" %)
  = (% class="highlight tag" %)${tag}##
      #if ($xwiki.hasAdminRights() && $displayButtons) ##
        [[$services.localization.render('xe.tag.rename.link')>>||queryString="do=prepareRename&tag=${urlEscapedTag}" class="button rename" rel="nofollow"]] [[$services.localization.render('xe.tag.delete.link')>>||queryString="do=prepareDelete&tag=${urlEscapedTag}" class="button delete" rel="nofollow"]]##
      #end
    (%%) =
#end
##
## Switch between all possible actions:
## viewTag, prepareRename, rename, prepareDelete, delete, default (Tag cloud)
##
#if ($do == 'viewTag')
  ##
  ## View tag
  ##
  #displayTagAppTitle($urlEscapedTag $htmlEscapedTag true)
  #if ("$!{request.get('renamedTag')}" != '')
    {{info}}$services.localization.render('xe.tag.rename.success', ["//${request.get('renamedTag')}//"]){{/info}}

  #end
  #set ($list = $xwiki.tag.getDocumentsWithTag($tag))
  {{container layoutStyle="columns"}}
    (((
      (% class="xapp" %)
      === $services.localization.render('xe.tag.alldocs', ["//${tag}//"]) ===

      #if ($list.size()> 0)
        {{html}}#displayDocumentList($list false $blacklistedSpaces){{/html}}
      #else
        (% class='noitems' %)$services.localization.render('xe.tag.notags')
      #end
    )))
    (((
      (% class="xapp" %)
      === $services.localization.render('xe.tag.activity', ["//${tag}//"]) ===
      {{notifications useUserPreferences="false" displayOwnEvents="true" tags="$tag" displayRSSLink="true" /}}
    )))
  {{/container}}
#elseif ($do == 'prepareRename')
  ##
  ## Prepare rename tag
  ##
  #displayTagAppTitle($urlEscapedTag $htmlEscapedTag false)
{{html}}
  <form id="renameForm" action="$doc.getURL()" method="post">

    <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
    <input name="do" type="hidden" value="renameTag" />
    <input name="tag" type="hidden" value="$htmlEscapedTag" />
    $services.localization.render('xe.tag.rename.renameto', [$htmlEscapedTag]) <input type="text" name="renameTo" /> <input type="submit" value="$services.localization.render('xe.tag.rename')" class="button"/>$services.localization.render('cancel')

  </form>
{{/html}}
#elseif ($do == 'renameTag')
  ##
  ## Rename tag
  ##
  #set ($renameTo = "$!{request.get('renameTo')}")
  #set ($success = false)
  #if ($renameTo != '')
    #set ($success = $xwiki.tag.renameTag($tag, $renameTo))
  #end
  #if ($success == true || $success == 'OK')
   #set ($urlEscapedRenameTo = $escapetool.url($renameTo))
   $response.sendRedirect($doc.getURL('view', "do=viewTag&tag=${urlEscapedRenameTo}&renamedTag=${urlEscapedTag}"))
  #else
    {{error}}$services.localization.render('xe.tag.rename.failure', ["//${tag}//", "//${renameTo}//"]){{/error}}
  #end
#elseif ($do == 'prepareDelete')
  ##
  ## Prepare delete tag
  ##
  #displayTagAppTitle($urlEscapedTag $htmlEscapedTag false)
{{html}}
  <form id="deleteForm" action="$doc.getURL()" method="post">

    <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" />
    <input name="do" type="hidden" value="deleteTag" />
    <input name="tag" type="hidden" value="$htmlEscapedTag" />
    <input type="submit" value="$services.localization.render('xe.tag.delete', [$htmlEscapedTag])" class="button"/>$services.localization.render('cancel')

  </form>
{{/html}}
#elseif ($do == 'deleteTag')
  ##
  ## Delete tag
  ##
  #set ($success = $xwiki.tag.deleteTag($tag))
  #if ($success == true || $success == 'OK')
    $response.sendRedirect($doc.getURL('view', "deletedTag=${urlEscapedTag}"))
  #else
    {{error}}$services.localization.render('xe.tag.delete.failure', ["//${tag}//"]){{/error}}
  #end
#else
  ##
  ## View all tags (Tag Cloud)
  ##
  #set ($title = 'All Tags')
  #if ("$!{request.get('deletedTag')}" != '')
    {{info}}$services.localization.render('xe.tag.delete.success', ["//${request.get('deletedTag')}//"]){{/info}}

  #end
  {{tagcloud/}}
#end
#set ($displayDocExtra = false)
{{/velocity}}</content>
  <object>
    <name>Main.Tags</name>
    <number>0</number>
    <className>XWiki.StyleSheetExtension</className>
    <guid>842b2aaf-cc25-4c1d-b0de-c23318aa9cbe</guid>
    <class>
      <name>XWiki.StyleSheetExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>6</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <disabled>0</disabled>
        <name>code</name>
        <number>3</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <contentType>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>contentType</name>
        <number>1</number>
        <prettyName>Content Type</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>CSS|LESS</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentType>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>2</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>5</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>4</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      #template('colorThemeInit.vm')

h1.xapp {
  border-bottom: 0 none;
  margin: 0;
}

h1.xapp + form {
  margin: 1.5em 0 1em;
}

h1.xapp * {
  vertical-align: middle;
}

h1.xapp .highlight {
  background: url("$xwiki.getSkinFile('icons/silk/tag_blue.png')") no-repeat 5px 50% $theme.backgroundSecondaryColor;
  border: solid 1px $theme.borderColor;
  border-radius: 10px;
  display: inline-block;
  font-weight: bold;
  padding: 2px 8px 2px 25px;
  white-space: nowrap;
}

h1.xapp .button {
  background: no-repeat 3px 50% $theme.pageContentBackgroundColor;
  border: solid 1px $theme.borderColor;
  border-radius: 7px;
  color: $theme.linkColor;
  display : inline-block;
  font-size: 50%;
  font-weight: normal;
  line-height: 1.5em;
  padding: 2px 5px 2px 20px;
}

h1.xapp .delete {
  background-image: url("$xwiki.getSkinFile('icons/silk/cross.png')");
}

h1.xapp .rename {
  background-image: url("$xwiki.getSkinFile('icons/silk/textfield_rename.png')");
}

h1.xapp .button:hover {
  background-color: $theme.highlightColor;
  text-decoration: none;
}

    </property>
    <property>
      <name/>
    </property>
    <property>
      <parse>1</parse>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
</xwikidoc>

Output:
【漏洞类型】XSS（跨站脚本攻击）
【严重性】Medium
【漏洞摘要】这是一个XSS漏洞。原始代码中，用户输入的`$tag`和`$renamedTag`变量在不进行适当的转义的情况下直接嵌入到HTML输出中。攻击者可以通过提交恶意的标签名称来注入恶意脚本，从而在用户浏览器中执行任意JavaScript代码，这可能导致敏感信息泄露、会话劫持等安全问题。
【关键代码模式】与此漏洞相关的关键代码模式包括：
- `#set ($do = "$!{request.get('do')}")`
- `#set ($tag = "$!{request.get('tag')}")`
- `#set ($renamedTag = "$!{request.get('renamedTag')}")`
- `{{info}}...{{/info}}`
- `{{error}}...{{/error}}`
- `{{html}}...{{/html}}`
【修复方法说明】修复方法通过使用`$services.rendering.escape`函数对用户输入的标签名称进行转义，解决了XSS漏洞问题。最佳实践是始终对用户输入进行适当的转义处理，特别是在动态生成HTML内容时。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-index/xwiki-platform-index-ui/src/main/resources/XWiki/DeletedAttachments.xml
### 代码内容：
<?xml version="1.1" encoding="UTF-8"?>

<xwikidoc version="1.3" reference="XWiki.DeletedAttachments" locale="">
  <web>XWiki</web>
  <name>DeletedAttachments</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>Main.AllDocs</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>$services.localization.render('platform.index.attachmentsTrash')</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.0</syntaxId>
  <hidden>true</hidden>
  <content>{{velocity}}
#set($dateFormat = 'yyyy MMMM d, HH:mm')
#if(!$request.list)
$xwiki.ssx.use('XWiki.DeletedAttachments')##
$xwiki.jsx.use('XWiki.DeletedAttachments', {'minify' : 'false'})##
##
#set($columns = ['datt.filename', 'datt.docName', 'datt.date', 'datt.deleter', 'actions'])
#set($columnsProperties = {
    'datt.filename'  : { 'type' : 'text', 'size' : 10 },
    'datt.docName'   : { 'type' : 'text', 'size' : 10 },
    'datt.date'      : { 'type' : 'date'},
    'datt.deleter'   : { 'type' : 'text', 'size' : 10 },
    'actions'        : { 'type' : 'text', 'filterable' : false, 'sortable' : false }
  })##
#set($options = {
    'url'               : "$xwiki.getURL('XWiki.DeletedAttachments', 'view', 'list=1&xpage=plain&outputSyntax=plain')",
    'callback'          : 'XWiki.index.trash.attachments.displayEntry',
    'translationPrefix' : 'platform.index.trashAttachments.'
  })##
##
##
##
{{html wiki="true" clean="false"}}
<div class="hidden">
#livetable('attachmentTrash' $columns $columnsProperties $options)
</div>
{{/html}}

{{html wiki="true" clean="false"}}
<noscript>

  #set($deletedAttachments = $services.query.hql('select count(datt.id) from DeletedAttachment as datt').execute().get(0))
  #if($deletedAttachments == 0)

    {{info}}$services.localization.render('platform.index.trashAttachmentsEmpty'){{/info}}

  #else
  #macro(displayDeletedAttachment $id)
    #set($datt = $xwiki.getDeletedAttachment("$id"))
    #set($originalAttachment = $datt.getAttachment())
    #set($originalDocument = $xwiki.getDocument($datt.docName))
    <tr>
      <td><a href="$originalDocument.getAttachmentRevisionURL($originalAttachment.filename, $originalAttachment.version, "rid=${datt.id}").replaceAll('&', '&amp;')">$escapetool.xml($datt.filename)</a></td>
      <td><a href="$originalDocument.getURL()">$escapetool.xml($originalDocument.plainTitle) ($escapetool.xml(${originalDocument.fullName}))</a></td>
      <td>$xwiki.formatDate($datt.getDate(), $dateFormat)</td>
      <td>$xwiki.getUserName($datt.getDeleter())</td>
      <td class="itemActions">
        #if($datt.canDelete())
          <a href="${originalDocument.getAttachmentURL($datt.filename, 'delattachment', "trashId=${datt.id}&amp;xredirect=$escapetool.url($!request.getRequestURI())?$escapetool.url($!request.getQueryString())")}" class="tool delete" title="$services.localization.render('platform.index.trashAttachmentsActionsDeleteTooltip')">$services.localization.render('platform.index.trashAttachmentsDeleteText')</a>
        #end
      </td>
    </tr>
  #end
##
  #if("$!{request.view}" == '')
    #set($queryParams = '')
  #else
    #set($queryParams = "view=$!{escapetool.url($request.view)}")
  #end
  #set($paginationParameters = {
    'totalItems' : $services.query.hql('select count(datt.id) from DeletedAttachment as datt').execute().get(0),
    'defaultItemsPerPage' : 15,
    'url' : $doc.getURL('view', $queryParams)
  })
  #pagination($paginationParameters)
  <table id="searchResults" class="xwiki-livetable xwiki-livetable-display">
    <thead class="xwiki-livetable-display-header"><tr>
      <th scope="col" class="xwiki-livetable-display-header-text selected asc">$services.localization.render('platform.index.trashAttachments.datt.filename')</th>
      <th scope="col" class="xwiki-livetable-display-header-text">$services.localization.render('platform.index.trashAttachments.datt.docName')</th>
      <th scope="col" class="xwiki-livetable-display-header-text">$services.localization.render('platform.index.trashAttachments.datt.date')</th>
      <th scope="col" class="xwiki-livetable-display-header-text">$services.localization.render('platform.index.trashAttachments.datt.deleter')</th>
      <th scope="col" class="xwiki-livetable-display-header-text">$services.localization.render('platform.index.trashAttachments.actions')</th>
    </tr></thead>
    <tbody class="xwiki-livetable-display-body">
    #set ($query = $services.query.hql('select datt.id from DeletedAttachment as datt order by datt.filename asc'))
    #foreach($item in $query.setLimit($paginationParameters.itemsPerPage).setOffset($paginationParameters.firstItem).execute())
      #displayDeletedAttachment($item)
    #end
    </tbody>
  </table>
  #set($discard = $paginationParameters.put('position', 'bottom'))
  #pagination($paginationParameters)
  #end ## deletedAttachments != 0
</noscript>
{{/html}}##
#end## !$request.list
{{/velocity}}

{{velocity output="false"}}
## ============================================================================================
## This page is called from live table via Ajax with the argument xpage=plain. It returns a
## set of results serialized in JSON.
## @programming This service page needs to be saved by a user with the programming access level
## to work as expected
## ============================================================================================
#if ($request.list && $request.xpage == 'plain')
  #set ($offset = $numbertool.toNumber($request.get('offset')).intValue())
  ## offset starts from 0 in velocity and 1 in javascript
  #set ($offset = $offset - 1)
  #if (!$offset || $offset < 0)
    #set ($offset = 0)
  #end
  #set ($limit = $numbertool.toNumber($request.get('limit')).intValue())
  #if (!$limit)
    #set ($limit = 15)
  #end
  #set ($filenameFilter = $request.get('datt.filename'))
  #set ($docNameFilter = $request.get('datt.docName'))
  #set ($dateFilter = $request.get('datt.date'))
  #set ($deleterFilter = $request.get('datt.deleter'))
  #set ($sql = '')
  #set ($params = {})
  #if ("$!filenameFilter" != '')
    #set ($sql = "and lower(datt.filename) like '%${filenameFilter.trim().toLowerCase()}%' ")
  #end
  #if ("$!docNameFilter" != '')
    #set ($sql = "and lower(datt.docName) like '%${docNameFilter.trim().toLowerCase()}%' ")
  #end
  #if ("$!dateFilter" != '')
    #set ($dates = $dateFilter.split('-'))
    #if ($dates.size() == 2)
      ## Date range matching
      #set ($sql = "${sql} and datt.date between :startDate and :endDate ")
      #set ($discard = $params.put('startDate', $datetool.toDate($numbertool.toNumber($dates[0]))))
      #set ($discard = $params.put('endDate', $datetool.toDate($numbertool.toNumber($dates[1]))))
    #else
      ## Single value matching
      #set ($sql = "${sql} and upper(str(datt.date)) like upper(str(:date)) ")
      #set ($discard = $params.put('date', $dateFilter))
    #end
  #end
  #if ("$!deleterFilter" != '')
    #set ($sql = $sql + "and lower(datt.deleter) like '%${deleterFilter.trim().toLowerCase()}%' ")
  #end
  #if ("$!request.sort" != '')
    #set ($order = $request.sort)
  #else
    #set ($order = 'datt.id')
  #end
  #if ("$!request.dir" != '')
    #set ($dir = $request.dir)
  #else
    #set ($dir = 'asc')
  #end
  #set ($selectSql = "select datt.id, $order from DeletedAttachment as datt where 1=1 " + $sql + " order by $order $dir")
  #set ($countSql = "select count(*) from DeletedAttachment as datt where 1=1 " + $sql)
  #set ($items = $services.query.hql($selectSql).bindValues($params).setLimit($limit).setOffset($offset).execute())
  #set ($totalItems = $services.query.hql($countSql).bindValues($params).execute().get(0))
  #set ($json = {
    'totalrows': $totalItems,
    'returnedrows': $items.size(),
    'offset': $mathtool.add($offset, 1),
    'reqNo': $request.reqNo,
    'query': $selectSql,
    "rows": []
  })
  #foreach ($item in $items)
    #set ($datt = $xwiki.getDeletedAttachment("$item.get(0)"))
    #set ($originalDocument = $xwiki.getDocument($datt.docName))
    #set ($row = {
      'filename': $datt.filename,
      'docName': $datt.docName,
      'documentUrl': $xwiki.getURL($datt.docName),
      'canDelete': $datt.canDelete(),
      'canRestore': $datt.canRestore(),
      'restoreUrl': $xwiki.getAttachmentURL($datt.docName, $datt.filename),
      'date': $xwiki.formatDate($datt.date, $dateFormat),
      'deleter': $datt.deleter,
      'deletername': $xwiki.getUserName($datt.deleter, false),
      'deleterurl': $xwiki.getURL($datt.deleter, 'view')
    })
    #set ($row.alreadyExists = "$!originalDocument.getAttachment($datt.filename)" != '')
    #if ($originalDocument)
      #set ($row.title = $originalDocument.plainTitle)
      #set ($row.deleteUrl = $originalDocument.getAttachmentURL($datt.filename, 'delattachment', $escapetool.url({
        'trashId': $datt.id,
        'form_token': $services.csrf.token
      })))
      #set ($originalAttachment = $datt.getAttachment())
      #if ($originalAttachment)
        #set ($row.url = $originalDocument.getAttachmentRevisionURL($originalAttachment.filename, $originalAttachment.version, $escapetool.url({
          'rid': $datt.id
        })))
      #end
    #else
      #set ($row.title = $datt.docName)
    #end
    #set ($discard = $json.rows.add($row))
  #end
  #jsonResponse($json)
#end
{{/velocity}}</content>
  <object>
    <name>XWiki.DeletedAttachments</name>
    <number>0</number>
    <className>XWiki.JavaScriptExtension</className>
    <guid>2b1a4426-e37b-40ba-9ad3-4055a466da0a</guid>
    <class>
      <name>XWiki.JavaScriptExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>5</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>code</name>
        <number>2</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>4</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>3</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      // Make sure the XWiki.index.trash.attachments 'namespace' exists.
if (typeof XWiki == "undefined") {
  XWiki = new Object();
}
if (typeof XWiki.index == "undefined") {
  XWiki.index = new Object();
}
if (typeof XWiki.index.trash == "undefined") {
  XWiki.index.trash = new Object();
}
if (typeof XWiki.index.trash.attachments == "undefined") {
  XWiki.index.trash.attachments = new Object();
}

/**
 * Callback function used by the DeletedAttachments livetable for generating the HTML code needed to display an entry.
 *
 * @param row the JSON data corresponding to the entry to be displayed
 * @param i the index of the row in the list of entries
 * @param table the LiveTable javascript object
 */
XWiki.index.trash.attachments.displayEntry = function (row, i, table) {
  var tr = new Element('tr'); // The resulting table row
  var file = new Element('a', {'href' : row.url}).update(row.filename);
  tr.appendChild(new Element('td').update(file));
  var doc = new Element('a', {'href' : row.documentUrl}).update(row.title);
  doc.appendChild(document.createTextNode(' (' + row.docName + ')'));
  tr.appendChild(new Element('td').update(doc));
  tr.appendChild(new Element('td').update(row.date));
  var deleter = new Element('a', {'href' : row.deleterurl}).update(row.deletername);
  tr.appendChild(new Element('td').update(deleter));
  var actions = new Element('td', {'class' : 'itemActions'});
  if(row.canDelete) {
    var delete_ = new Element('a', {
        'href' : row.deleteUrl,
        'class' : "tool delete",
        'title' : "$services.localization.render('platform.index.trashAttachmentsActionsDeleteTooltip')"
      }).update("$services.localization.render('platform.index.trashAttachmentsActionsDeleteText')");
    actions.appendChild(delete_);
    delete_.observe('click', XWiki.index.trash.attachments.confirmDelete.bindAsEventListener(delete_, table, i));
  }
  tr.appendChild(actions);
  return tr;
}

/**
 * Event listener, called when clicking a delete version link. It displays a confirmation box, speeding up the two-step
 * deletion process.
 *
 * @param event the link activation event
 * @param table the LiveTable javascript object
 * @param rowIdx the index of the row corresponding to the entry to be deleted
 */
XWiki.index.trash.attachments.confirmDelete = function(event, table, rowIdx) {
  event.stop();
  if (this.disabled) {
    // Do nothing if the button was already clicked and it's waiting for a response from the server.
    return;
  } else {
    new XWiki.widgets.ConfirmedAjaxRequest(
      /* Ajax request URL */
      this.href + '&confirm=1' + (Prototype.Browser.Opera ? "" : "&ajax=1"),
      /* Ajax request parameters */
      {
        onCreate : function() {
          // Disable the button, to avoid a cascade of clicks from impatient users
          this.disabled = true;
        }.bind(this),
        onSuccess : function(table, rowIdx) {
          // Remove the corresponding HTML element from the UI and update the livetable
          table.deleteRow(rowIdx);
        }.bind(this, table, rowIdx),
        // This is an expected 404, returned when the document has been successfully deleted and is no longer found
        on404 : function(response, table, rowIdx) {
          response.request.options.onSuccess(table, rowIdx);
        }.bindAsEventListener(this, table, rowIdx),
        onComplete : function() {
          // In the end: re-enable the button
          this.disabled = false;
        }.bind(this)
      },
      /* Interaction parameters */
      {
         confirmationText: "$services.localization.render('core.recyclebin.completelyDeleteConfirm')",
         progressMessageText : "$services.localization.render('platform.index.trashAttachmentsDeleteInProgress')",
         successMessageText : "$services.localization.render('platform.index.trashAttachmentsDeleteDone')",
         failureMessageText : "$services.localization.render('platform.index.trashAttachmentsDeleteFailed')"
      }
      );
  }
}
/*
 * The livetable is hidden when javascript is disabled, so that an alternative <noscript> table is displayed.
 * Re-display it when the document loads and Javascript is available.
 */
document.observe('xwiki:dom:loaded', function() {
  if ($('attachmentTrash')) {
    $('attachmentTrash').up('.hidden').removeClassName('hidden');
  }
});
    </property>
    <property>
      <name/>
    </property>
    <property>
      <parse>1</parse>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
  <object>
    <name>XWiki.DeletedAttachments</name>
    <number>0</number>
    <className>XWiki.StyleSheetExtension</className>
    <guid>33778e4e-5d86-472e-b065-c9c87b5b282e</guid>
    <class>
      <name>XWiki.StyleSheetExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>5</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>code</name>
        <number>2</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <contentType>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>contentType</name>
        <number>6</number>
        <prettyName>Content Type</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>CSS|LESS</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentType>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>4</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>3</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      .itemActions .tool {
  width: 16px;
  height: 16px;
  margin: 0 2px;
  float: left;
  text-indent: -1000em;
  overflow: hidden;
  background: transparent no-repeat center center;
}
.itemActions .delete {
  background-image: url("$xwiki.getSkinFile('icons/silk/cross.png')") !important;
}
.itemActions .restore {
   background-image: url("$xwiki.getSkinFile('icons/silk/arrow_refresh.png')") !important;
}
.itemActions .cannot-restore {
   background-image: url("$xwiki.getSkinFile('icons/silk/page_error.png')") !important;
}
    </property>
    <property>
      <contentType>CSS</contentType>
    </property>
    <property>
      <name/>
    </property>
    <property>
      <parse>1</parse>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
  <object>
    <name>XWiki.DeletedAttachments</name>
    <number>0</number>
    <className>XWiki.XWikiRights</className>
    <guid>8d68ba7a-72d3-4f9f-a984-c7c47139b0a8</guid>
    <class>
      <name>XWiki.XWikiRights</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <allow>
        <defaultValue>1</defaultValue>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>allow</displayType>
        <name>allow</name>
        <number>4</number>
        <prettyName>Allow/Deny</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </allow>
      <groups>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>input</displayType>
        <multiSelect>1</multiSelect>
        <name>groups</name>
        <number>1</number>
        <picker>1</picker>
        <prettyName>Groups</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <size>5</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.GroupsClass</classType>
      </groups>
      <levels>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <multiSelect>1</multiSelect>
        <name>levels</name>
        <number>2</number>
        <prettyName>Levels</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <size>3</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.LevelsClass</classType>
      </levels>
      <users>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>input</displayType>
        <multiSelect>1</multiSelect>
        <name>users</name>
        <number>3</number>
        <picker>1</picker>
        <prettyName>Users</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <size>5</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.UsersClass</classType>
      </users>
    </class>
    <property>
      <allow>1</allow>
    </property>
    <property>
      <groups>XWiki.XWikiAdminGroup</groups>
    </property>
    <property>
      <levels>view,edit</levels>
    </property>
    <property>
      <users/>
    </property>
  </object>
</xwikidoc>