import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.applet.*;
import java.net.*;
import java.util.*;
import netscape.javascript.JSObject;

// A java filemanager that allows the user to manipulate files on the
// Webmin server. Layout is similar to the windows explorer - directory
// tree on the left, files on the right, action buttons on the top.
public class FileManager extends Applet
	implements CbButtonCallback, HierarchyCallback, MultiColumnCallback
{
	// top buttons
	CbButton ret_b, config_b, down_b, edit_b, refresh_b, props_b,
		 copy_b, cut_b, paste_b, delete_b, new_b, upload_b, mkdir_b,
		 makelink_b, rename_b, share_b, mount_b, search_b, acl_b,
		 attr_b, ext_b, preview_b, extract_b, hnew_b;

	// Directory tree
	Hierarchy dirs;
	FileNode root;
	Hashtable nodemap = new Hashtable();

	// File list
	MultiColumn files;
	TextField pathname;
	CbButton history_b;
	RemoteFile showing_files;
	RemoteFile showing_list[];
	Vector history_list = new Vector();

	// Copying and pasting
	RemoteFile cut_buffer[];
	boolean cut_mode;

	static final String monmap[] = { "Jan", "Feb", "Mar", "Apr",
					 "May", "Jun", "Jul", "Aug",
					 "Sep", "Oct", "Nov", "Dec" };
	String accroot[];
	String accnoroot[];
	Hashtable lang = new Hashtable();
	Hashtable stab = new Hashtable(),
		  ntab = new Hashtable();
	boolean sambamode;
	int nfsmode;
	String trust;
	String extra;
	String images;
	int iconsize;

	boolean got_filesystems,
		acl_support, attr_support, ext_support;
	Hashtable mounts = new Hashtable();
	Vector fslist = new Vector();
	boolean read_only = false;

	// Standard font for all text
	Font fixed;

	// Font for button labels
	Font small_fixed;

	// Full session cookie
	String session;

	// HTTP referer
	String referer;

	// Archive parameter
	String archive;

	// Chroot directory for tree
	String chroot;

	// File attributes that can be edited
	boolean can_perms, can_users;

	// Symlimks are automatically followed
	boolean follow_links;

	// Can search file contents
	boolean search_contents;

	// Use text editor for HTML
	boolean force_text;

	// File extensions to consider as HTML
	String htmlexts[];

	public void init()
	{
	setLayout(new BorderLayout());

	// Create fonts from specified size
	fixed = make_font("fixed", 12);
	small_fixed = make_font("small_fixed", 10);

	Util.setFont(small_fixed);
	StringTokenizer tok = new StringTokenizer(getParameter("root"), " ");
	accroot = new String[tok.countTokens()];
	for(int i=0; tok.hasMoreTokens(); i++)
		accroot[i] = tok.nextToken();
	if (getParameter("noroot") != null) {
		tok = new StringTokenizer(getParameter("noroot"), " ");
		accnoroot = new String[tok.countTokens()];
		for(int i=0; tok.hasMoreTokens(); i++)
			accnoroot[i] = tok.nextToken();
		}
	else {
		accnoroot = new String[0];
		}
	trust = getParameter("trust");
	session = getParameter("session");
	referer = getDocumentBase().toString();
	extra = getParameter("extra");
	if (extra == null) extra = "";
	images = getParameter("images");
	if (images == null) images = "images";
	iconsize = Integer.parseInt(getParameter("iconsize"));
	archive = getParameter("doarchive");
	if (archive == null) archive = "0";
	chroot = getParameter("chroot");
	if (chroot == null) chroot = "/";
	String can_perms_str = getParameter("canperms");
	can_perms = can_perms_str == null || !can_perms_str.equals("0");
	String can_users_str = getParameter("canusers");
	can_users = can_users_str == null || !can_users_str.equals("0");
	String search_contents_str = getParameter("contents");
	search_contents = search_contents_str == null ||
			  !search_contents_str.equals("0");
	String force_text_str = getParameter("force_text");
	if (force_text_str != null && force_text_str.equals("1"))
		force_text = true;
	String htmlexts_str = getParameter("htmlexts");
	if (htmlexts_str == null || htmlexts_str.equals(""))
		htmlexts_str = ".htm .html";
	htmlexts = DFSAdminExport.split(htmlexts_str);

	// download language strings
	String l[] = get_text("lang.cgi");
	if (l.length < 1 || l[0].indexOf('=') < 0) {
		String err = "Failed to get language list : "+join_array(l);
		new ErrorWindow(err);
		throw new Error(err);
		}
	for(int i=0; i<l.length; i++) {
		int eq = l[i].indexOf('=');
		if (eq >= 0)
			lang.put(l[i].substring(0, eq), l[i].substring(eq+1));
		}

	// list samba file shares
	String s[] = get_text("list_shares.cgi");
	if (s[0].equals("1")) {
		for(int i=1; i<s.length; i++) {
			SambaShare ss = new SambaShare(s[i]);
			stab.put(ss.path, ss);
			}
		sambamode = true;
		}

	// list NFS exports
	String e[] = get_text("list_exports.cgi");
	nfsmode = e.length == 0 ? 0 : Integer.parseInt(e[0]);
	if (nfsmode != 0) {
		for(int i=1; i<e.length; i++) {
			if (nfsmode == 1) {
				// Linux export
				LinuxExport le = new LinuxExport(e[i]);
				ntab.put(le.path, le);
				}
			else if (nfsmode == 2) {
				// Solaris share
				DFSAdminExport de = new DFSAdminExport(e[i]);
				ntab.put(de.path, de);
				}
			}
		}

	// list filesystems
	get_filesystems();

	// get read-only flag
	if (getParameter("ro").equals("1"))
		read_only = true;

	// get custom colours
	Util.light_edge = get_colour("light_edge", Util.light_edge);
	Util.dark_edge = get_colour("dark_edge", Util.dark_edge);
	Util.body = get_colour("body", Util.body);
	Util.body_hi = get_colour("body_hi", Util.body_hi);
	Util.light_edge_hi = get_colour("light_edge_hi", Util.light_edge_hi);
	Util.dark_edge_hi = get_colour("dark_edge_hi", Util.dark_edge_hi);
	Util.dark_bg = get_colour("dark_bg", Util.dark_bg);
	Util.text = get_colour("text", Util.text);
	Util.light_bg = get_colour("light_bg", Util.light_bg);

	// create button panel
	BorderPanel top = new BorderPanel(2, Util.body);
	top.setLayout(new ToolbarLayout(ToolbarLayout.LEFT, 5, 2));

	Panel top1 = new Panel();
	top1.setLayout(new GridLayout(1, 0));
	if (getParameter("return") != null && can_button("return"))
		top1.add(ret_b = make_button("ret.gif", text("top_ret")));
	if (getParameter("config") != null && can_button("config"))
		top1.add(config_b = make_button("config.gif",
						text("top_config")));
	if (can_button("save"))
		top1.add(down_b = make_button("down.gif", text("top_down")));
	if (can_button("preview"))
		top1.add(preview_b = make_button("preview.gif", text("top_preview")));
	if (!read_only && can_button("edit")) {
		top1.add(edit_b = make_button("edit.gif", text("top_edit")));
		}
	if (can_button("refresh"))
		top1.add(refresh_b = make_button("refresh.gif", text("top_refresh")));
	if (!read_only && can_button("info"))
		top1.add(props_b = make_button("props.gif", text("top_info")));
	if (acl_support && !read_only && can_button("acl"))
		top1.add(acl_b = make_button("acl.gif", text("top_eacl")));
	if (attr_support && !read_only && can_button("attr"))
		top1.add(attr_b = make_button("attr.gif", text("top_attr")));
	if (ext_support && !read_only && can_button("ext"))
		top1.add(ext_b = make_button("ext.gif", text("top_ext")));
	if (can_button("search"))
		top1.add(search_b = make_button("search.gif", text("top_search")));
	top.add(top1);
	
	if (!read_only) {
		Panel top2 = new Panel();
		top2.setLayout(new GridLayout(1, 0));
		if (can_button("delete"))
			top2.add(delete_b = make_button("delete.gif",
							text("top_delete")));
		if (can_button("new")) {
			top2.add(new_b = make_button("new.gif",
						     text("top_new")));
			if (can_button("htmlnew"))
				top2.add(hnew_b = make_button("html.gif",
							     text("top_new")));
			}
		if (can_button("upload"))
			top2.add(upload_b = make_button("upload.gif",
							text("top_upload")));
		if (can_button("extract"))
			top2.add(extract_b = make_button("extract.gif",
							 text("top_extract")));
		if (can_button("mkdir"))
			top2.add(mkdir_b = make_button("mkdir.gif",
						       text("top_new")));
		if (getParameter("follow").equals("0") &&
		    can_button("makelink"))
			top2.add(makelink_b = make_button("makelink.gif",
						text("top_new")));
		if (can_button("rename"))
			top2.add(rename_b = make_button("rename.gif",
							text("top_rename")));
		if ((sambamode || nfsmode != 0) &&
		    getParameter("sharing").equals("1") &&
		    can_button("sharing"))
			top2.add(share_b = make_button("share.gif",
						       text("top_share")));
		if (getParameter("mounting").equals("1") &&
		    can_button("mount"))
			top2.add(mount_b = make_button("mount.gif",
						       text("top_mount")));
		top.add(top2);

		if (can_button("copy")) {
			Panel top3 = new Panel();
			top3.setLayout(new GridLayout(1, 0));
			top3.add(copy_b = make_button("copy.gif",
						      text("top_copy")));
			top3.add(cut_b = make_button("cut.gif",
						     text("top_cut")));
			top3.add(paste_b = make_button("paste.gif",
						       text("top_paste")));
			top.add(top3);
			}
		}
	add("North", top);
	follow_links = getParameter("follow").equals("1");

	// create directory tree
	BorderPanel left = new BorderPanel(2, Util.body);
	left.setLayout(new BorderLayout());
	root = new FileNode(new RemoteFile(this, get_text("root.cgi")[0],null));
	left.add("Center", dirs = new Hierarchy(root, this));
	dirs.setFont(fixed);
	root.open = true; root.fill();

	// create file list window
	BorderPanel right = new BorderPanel(2, Util.body);
	right.setLayout(new BorderLayout());
	Panel rtop = new Panel();
	rtop.setLayout(new BorderLayout());
	rtop.add("Center", pathname = new TextField());
	rtop.add("East", history_b = new CbButton(text("history_button"),this));
	right.add("North", rtop);
	pathname.setFont(fixed);
	String cols[] = { "", text("right_name"), text("right_size"),
			  text("right_user"), text("right_group"),
			  text("right_date") };
	float widths[] = { .07f, .33f, .15f, .15f, .15f, .15f };
	right.add("Center", files = new MultiColumn(cols, this));
	files.setWidths(widths);
	files.setDrawLines(false);
	files.setMultiSelect(true);
	files.setFont(fixed);
	show_files(root.file);

	ResizePanel mid = new ResizePanel(left, right, .3, false);
	add("Center", mid);

	// Go to the restricted directory
	String home = getParameter("home");
	String go = getParameter("goto");
	String open = getParameter("open");
	if (open != null) {
		find_directory(open, true);
		}
	else if (go != null && go.equals("1")) {
		if (home != null)
			find_directory(home, true);
		else if (!accroot[0].equals("/"))
			find_directory(accroot[0], true);
		}
	}

	Font make_font(String name, int defsize)
	{
	String str = getParameter(name);
	int size = str == null || str.equals("") ? defsize :
		   Integer.parseInt(str);
	return new Font("courier", Font.PLAIN, size);
	}

	// Looks up an applet parameter for a colour, and returns it if
	// defined, otherwise the default. MUST	be in RRGGBB hex format
	Color get_colour(String name, Color def)
	{
	String str = getParameter("applet_"+name);
	if (str == null) {
		return def;
		}
	else {
		return new Color(get_hex(str, 0),
				 get_hex(str, 2),
				 get_hex(str, 4));
		}
	}

	int get_hex(String str, int pos)
	{
	str = str.toUpperCase();
	char c1 = str.charAt(pos), c2 = str.charAt(pos+1);
	int b1 = Character.isDigit(c1) ? c1-48 : c1-65+10;
	int b2 = Character.isDigit(c2) ? c2-48 : c2-65+10;
	return (b1<<4) + (b2);
	}

	boolean can_button(String name)
	{
	return getParameter("no_"+name) == null;
	}

	CbButton make_button(String f, String t)
	{
	if (iconsize == 1)
		return new CbButton(get_image(f), this);
	else
		return new CbButton(get_image(f), t, CbButton.ABOVE, this);
	}

	// Gets an image from the images directory
	Image get_image(String img)
	{
	return getImage(getDocumentBase(), images+"/"+img);
	}

	// Gets charset parameter from Content-Type: header
	String get_charset(String ct)
	{
	if (ct == null)
		return null;
	StringTokenizer st = new StringTokenizer(ct, ";");
	while (st.hasMoreTokens()) {
		String l = st.nextToken().trim();
		if (l.startsWith("charset=")) {
			// get the value of charset= param.
			return l.substring(8);
			}
		}
	return null;
	}

	String[] get_text(String url)
	{
	try {
		long now = System.currentTimeMillis();
		if (url.indexOf('?') > 0) url += "&rand="+now;
		else url += "?rand="+now;
		url += "&trust="+trust;
		url += extra;
		URL u = new URL(getDocumentBase(), url);
		URLConnection uc = u.openConnection();
		set_cookie(uc);
		String charset = get_charset(uc.getContentType());
		InputStream ris = uc.getInputStream();
		BufferedReader is = null;
		if (charset == null) {
			is = new BufferedReader(new InputStreamReader(ris));
			}
		else {
			// Try to use a character set, and handle failure
			try {
				is = new BufferedReader(
					new InputStreamReader(ris, charset));
				}
			catch(Exception e) {
				e.printStackTrace();
				is = new BufferedReader(
					new InputStreamReader(ris));
				}
			}
		Vector lv = new Vector();
		while(true) {
			String l = is.readLine();
			if (l == null) { break; }
			lv.addElement(l);
			}
		is.close();
		String rv[] = new String[lv.size()];
		lv.copyInto(rv);
		return rv;
		}
	catch(Exception e) {
		e.printStackTrace();
		//return null;
		String err[] = { e.getClass().getName()+" : "+e.getMessage() };
		return err;
		}
	}

	void set_cookie(URLConnection conn)
	{
	if (session != null)
		conn.setRequestProperty("Cookie", session);
	conn.setRequestProperty("Referer", referer);
	}

	// Fill the multicolumn list with files from some directory
	boolean show_files(RemoteFile f)
	{
	RemoteFile fl[] = f.list();
	if (fl == null) return false;
	files.clear();
	Object rows[][] = new Object[fl.length+1][];
	long now = System.currentTimeMillis();

	// Sort listing by chosen column
	if (f != showing_files) {
		// Directory has changed .. assume sort by name
		files.sortingArrow(1, 1);
		}
	else if (files.sortdir != 0) {
		// Sort by chosen order
		RemoteFile fls[] = new RemoteFile[fl.length];
		System.arraycopy(fl, 0, fls, 0, fl.length);
		QuickSort.sort(fls, files.sortcol, files.sortdir);
		fl = fls;
		}

	// Create parent directory row
	rows[0] = new Object[6];
	rows[0][0] = get_image("dir.gif");
	rows[0][1] = "..";
	rows[0][2] = rows[0][3] = rows[0][4] = rows[0][5] = "";

	// Create file rows
	Date n = new Date(now);
	for(int i=0; i<fl.length; i++) {
		Object row[] = rows[i+1] = new Object[6];
		if (fl[i].shared() && fl[i].mounted())
			row[0] = get_image("smdir.gif");
		else if (fl[i].shared() && fl[i].mountpoint())
			row[0] = get_image("sudir.gif");
		else if (fl[i].shared())
			row[0] = get_image("sdir.gif");
		else if (fl[i].mounted())
			row[0] = get_image("mdir.gif");
		else if (fl[i].mountpoint())
			row[0] = get_image("udir.gif");
		else
			row[0] = get_image(RemoteFile.tmap[fl[i].type]);
		row[1] = fl[i].name;
		if (fl[i].size < 1000)
			row[2] = spad(fl[i].size, 5)+" B";
		else if (fl[i].size < 1000000)
			row[2] = spad(fl[i].size/1000, 5)+" kB";
		else
			row[2] = spad(fl[i].size/1000000, 5)+" MB";
		row[3] = fl[i].user;
		row[4] = fl[i].group;
		Date d = new Date(fl[i].modified);
		//if (now - fl[i].modified < 24*60*60*1000) {
		if (n.getDate() == d.getDate() &&
		    n.getMonth() == d.getMonth() &&
		    n.getYear() == d.getYear()) {
			// show as hour:min
			row[5] = pad(d.getHours(),2)+":"+
				 pad(d.getMinutes(),2);
			}
		//else if (now - fl[i].modified < 24*60*60*365*1000) {
		else if (n.getYear() == d.getYear()) {
			// show as day/mon
			row[5] = pad(d.getDate(),2)+"/"+
				 monmap[d.getMonth()];
			}
		else {
			// show as mon/year
			row[5] = monmap[d.getMonth()]+"/"+
				 pad(d.getYear()%100, 2);
			}
		}
	files.addItems(rows);
	showing_files = f;
	showing_list = fl;
	pathname.setText(f.path);
	return true;
	}

	String pad(int n, int s)
	{
	String rv = String.valueOf(n);
	while(rv.length() < s)
		rv = "0"+rv;
	return rv;
	}

	String spad(long n, int s)
	{
	String rv = String.valueOf(n);
	while(rv.length() < s)
		rv = " "+rv;
	return rv;
	}

	String trim_path(String p)
	{
	while(p.endsWith("/"))
		p = p.substring(0, p.length()-1);
	return p;
	}

	// openNode
	// Called when a node with children is opened
	public void openNode(Hierarchy h, HierarchyNode n)
	{
	FileNode fn = (FileNode)n;
	fn.fill();
	}

	// closeNode
	// Called when a node is closed
	public void closeNode(Hierarchy h, HierarchyNode n)
	{
	}

	// clickNode
	// Called when the user clicks on a node
	public void clickNode(Hierarchy h, HierarchyNode n)
	{
	FileNode fn = (FileNode)n;
	if (showing_files != fn.file)
		show_files(fn.file);
	}

	// doubleNode
	// Called when a user double-clicks on a node
	public void doubleNode(Hierarchy h, HierarchyNode n)
	{
	}

	// Called when a button is clicked
	public void click(CbButton b)
	{
	int s = files.selected();
	int ss[] = files.allSelected();
	RemoteFile f = null, ff[] = new RemoteFile[0];
	if (s > 0 || s == 0 && ss.length > 1) {
		// At least one non-.. file was selected
		boolean parentsel = false;
		for(int i=0; i<ss.length; i++)
			if (ss[i] == 0)
				parentsel = true;
		RemoteFile list[] = showing_list;
		if (parentsel) {
			// need to exclude .. from selected list!
			ff = new RemoteFile[ss.length-1];
			for(int i=0,j=0; i<ss.length; i++)
				if (ss[i] != 0)
					ff[j++] = list[ss[i]-1];
			f = s == 0 ? ff[0] : list[s-1];
			}
		else {
			// include all selected files
			f = list[s-1];
			ff = new RemoteFile[ss.length];
			for(int i=0; i<ss.length; i++)
				ff[i] = list[ss[i]-1];
			}
		}
	FileNode d = (FileNode)dirs.selected();
	if (b == ret_b) {
		// Return to the webmin index
		try {
			URL u = new URL(getDocumentBase(),
					getParameter("return"));
			getAppletContext().showDocument(u);
			}
		catch(Exception e) { }
		}
	else if (b == config_b) {
		// Open the module config window
		try {
			URL u = new URL(getDocumentBase(),
					getParameter("config"));
			getAppletContext().showDocument(u, "_self");
			}
		catch(Exception e) { }
		}
	else if (b == edit_b) {
		// Open a window for editing the selected file
		if (f == null)
			new ErrorWindow(text("top_efile"));
		else if (f.type == 0 || f.type > 4)
			new ErrorWindow(text("edit_enormal"));
		else if (is_html_filename(f.path) && !force_text) {
			// Open HTML editor
			try {
				JSObject win = JSObject.getWindow(this);
				String params[] = { f.path, "" };
				win.call("htmledit", params);
				}
			catch(Exception e) {
				new ErrorWindow(text("html_efailed",
						     e.getMessage()));
				}
			}
		else {
			// Open text editor
			new EditorWindow(f, this);
			}
		}
	else if (b == down_b) {
		// Force download of the selected file
		if (f == null) return;
		download_file(f);
		}
	else if (b == preview_b) {
		// Open preview window for selected file
		if (f == null) return;
		if (f.type == RemoteFile.DIR)
			new ErrorWindow(text("preview_eimage"));
		else
			new PreviewWindow(this, f);
		}
	else if (b == refresh_b) {
		// Refesh the selected directory (and thus any subdirs)
		if (d == null) return;
		d.refresh();
		show_files(d.file);
		}
	else if (b == props_b) {
		// Display the properties window
		if (f == null) return;
		new PropertiesWindow(f, this);
		}
	else if (b == acl_b) {
		// Display the ACL window (if filesystem supports them)
		if (f == null) return;
		FileSystem filefs = find_filesys(f);
		if (filefs == null) return;
		if (filefs.acls)
			new ACLWindow(this, f);
		else
			new ErrorWindow(text("eacl_efs", filefs.mount));
		}
	else if (b == attr_b) {
		// Display the attributes window (if filesystem supports them)
		if (f == null) return;
		FileSystem filefs = find_filesys(f);
		if (filefs == null) return;
		if (filefs.attrs)
			new AttributesWindow(this, f);
		else
			new ErrorWindow(text("attr_efs", filefs.mount));
		}
	else if (b == ext_b) {
		// Display EXT attributes window (if filesystem supports them)
		if (f == null) return;
		FileSystem filefs = find_filesys(f);
		if (filefs == null) return;
		if (filefs.ext)
			new EXTWindow(this, f);
		else
			new ErrorWindow(text("ext_efs", filefs.mount));
		}
	else if (b == copy_b) {
		// Copy the selected files
		if (f == null) return;
		cut_buffer = ff;
		cut_mode = false;
		}
	else if (b == cut_b) {
		// Cut the selected file
		if (f == null) return;
		cut_buffer = ff;
		cut_mode = true;
		}
	else if (b == paste_b) {
		// Paste the copied file
		if (cut_buffer == null) {
			new ErrorWindow(text("paste_ecopy"));
			return;
			}

		// Check for existing file clashes
		// XXX

		// Go through all the files to paste
		for(int i=0; i<cut_buffer.length; i++) {
			RemoteFile cf = cut_buffer[i];

			// Check for an existing file
			RemoteFile already = showing_files.find(cf.name);
			String sp = showing_files.path;
			String dest_path = sp.equals("/") ? sp+cf.name
							  : sp+"/"+cf.name;
			if (already != null) {
				// File exists .. offer to rename
				new OverwriteWindow(this, already, cf, i);
				}
			else {
				// do the move or copy
				RemoteFile nf = paste_file(cf, showing_files,
						   dest_path, null, cut_mode);
				if (cut_mode && nf != null) {
					// Paste from the destination path
					// from now on
					cut_buffer[i] = nf;
					}
				}
			}
		cut_mode = false;
		}
	else if (b == delete_b) {
		// Delete the selected files
		if (f == null) return;
		new DeleteWindow(this, ff);
		}
	else if (b == new_b) {
		// Open a window for creating a text file
		new EditorWindow(showing_files.path, this);
		}
	else if (b == hnew_b) {
		// Open a window for creating an HTML file
		try {
			JSObject win = JSObject.getWindow(this);
			String params[] = { "", showing_files.path };
			win.call("htmledit", params);
			}
		catch(Exception e) {
			new ErrorWindow(text("html_efailed",
					     e.getMessage()));
			}
		}
	else if (b == upload_b) {
		// Call javascript to open an upload window
		try {
			JSObject win = JSObject.getWindow(this);
			String params[] = { showing_files.path };
			win.call("upload", params);
			}
		catch(Exception e) {
			new ErrorWindow(text("upload_efailed", e.getMessage()));
			}
		}
	else if (b == extract_b) {
		// Ask for confirmation, then extract file
		if (f == null) return;
		if (f.type == 0 || f.type == 6 || f.type == 7)
			new ErrorWindow(text("extract_etype", f.path));
		else
			new ExtractWindow(this, f);
		}
	else if (b == mkdir_b) {
		// Prompt for new directory
		new MkdirWindow(showing_files.path, this);
		}
	else if (b == makelink_b) {
		// Prompt for a new symlink
		new LinkWindow(showing_files.path, this);
		}
	else if (b == rename_b) {
		// Prompt for new filename
		if (f == null) return;
		new RenameWindow(this, f);
		}
	else if (b == share_b) {
		// Open a window for editing sharing options
		if (f == null || f.type != RemoteFile.DIR) return;
		new SharingWindow(f, this);
		}
	else if (b == mount_b) {
		// Check if the selected directory is a mount point
		if (f == null || f.type != RemoteFile.DIR) return;
		FileSystem fs = f.fs();
		if (fs == null)
			new ErrorWindow(text("mount_epoint", f.path));
		else
			new MountWindow(this, fs, f);
		}
	else if (b == search_b) {
		// Open window for finding a file
		new SearchWindow(showing_files.path, this);
		}
	else if (b == history_b) {
		// Open entered file history window
		if (history_list.size() > 0) {
			new HistoryWindow(this);
			}
		}
	}

	boolean is_html_filename(String path)
	{
	for(int i=0; i<htmlexts.length; i++)
		if (path.toLowerCase().endsWith(htmlexts[i]))
			return true;
	return false;
	}

	boolean under_root_dir(String p, String roots[])
	{
	boolean can = false;
	int l = p.length();
	for(int r=0; r<roots.length; r++) {
		int rl = roots[r].length();
		if (roots[r].equals("/"))
			can = true;
		else if (l >= rl && p.substring(0, rl).equals(roots[r]))
			can = true;
		else if (l < rl && roots[r].substring(0, l).equals(p))
			can = true;
		}
	return can;
	}

	// Download some file to the user's browser, if possible
	void download_file(RemoteFile f)
	{
	if (f.type == RemoteFile.DIR && !archive.equals("0"))
		new DownloadDirWindow(this, f);
	else if (f.type == RemoteFile.DIR || f.type > 4)
		new ErrorWindow(text("view_enormal2"));
	else
		open_file_window(f, true, 0);
	}

	// Returns the object for some directory, or null if not found.
	RemoteFile find_directory(String p, boolean fill)
	{
	boolean can = under_root_dir(p, accroot) &&
		      !under_root_dir(p, accnoroot);
	if (!can) {
		new ErrorWindow(text("find_eaccess", p));
		return null;
		}
	FileNode posnode = root;
	RemoteFile pos = posnode.file;
	StringTokenizer tok = new StringTokenizer(p, "/");
	while(tok.hasMoreTokens()) {
		String fn = tok.nextToken();
		if (fn.equals("")) continue;
		RemoteFile fl[] = pos.list();
		if (fl == null) return null;
		if (fill) {
			posnode.open = true;
			posnode.fill();
			}
		boolean found = false;
		for(int i=0; i<fl.length; i++)
			if (fl[i].name.equals(fn)) {
				pos = fl[i];
				found = true;
				}
		if (!found) {
			new ErrorWindow(text("find_eexist", fn, p));
			return null;
			}
		if (pos.type != 0) {
			new ErrorWindow(text("find_edir", fn, p));
			return null;
			}
		if (fill)
			posnode = (FileNode)nodemap.get(pos);
		}
	if (fill) {
		if (show_files(pos)) {
			posnode.fill();
			posnode.open = true;
			dirs.select(posnode);
			dirs.redraw();
			}
		}
	return pos;
	}

	FileSystem find_filesys(RemoteFile f)
	{
	FileSystem filefs = null;
	for(int i=0; i<fslist.size(); i++) {
		FileSystem fs = (FileSystem)fslist.elementAt(i);
		int l = fs.mount.length();
		if (fs.mount.equals(f.path) ||
		    (f.path.length() >= l+1 &&
		     f.path.substring(0, l+1).equals(fs.mount+"/")) ||
		    fs.mount.equals("/")) {
			filefs = fs;
			}
		}
	return filefs;
	}

	public boolean action(Event e, Object o)
	{
	if (e.target == pathname) {
		// A new path was entered.. cd to it
		String p = pathname.getText().trim();
		if (p.equals("")) return true;
		find_directory(p, true);

		// Add to the history
		if (!history_list.contains(p)) {
			history_list.insertElementAt(p, 0);
			}
		return true;
		}
	return false;
	}

        // singleClick
        // Called on a single click on a list item
        public void singleClick(MultiColumn list, int num)
	{
	}

        // doubleClick
        // Called upon double-clicking on a list item
        public void doubleClick(MultiColumn list, int num)
	{
	if (num == 0) {
		// Go to parent directory
		if (showing_files.directory != null) {
			((FileNode)nodemap.get(showing_files)).open = false;
			show_files(showing_files.directory);
			dirs.select((FileNode)nodemap.get(showing_files));
			dirs.redraw();
			}
		return;
		}
	RemoteFile d = showing_list[num-1];
	if (d.type == 0) {
		// Open this directory
		FileNode pn = (FileNode)nodemap.get(showing_files);
		pn.fill();
		pn.open = true;
		FileNode fn = (FileNode)nodemap.get(d);
		if (show_files(d)) {
			fn.fill();
			fn.open = true;
			dirs.select(fn);
			dirs.redraw();
			}
		}
	else if (d.type <= 4) {
		// Direct the browser to this file
		open_file_window(d, list.last_event.shiftDown(), 0);
		}
	}

	// Called when the user clicks on a column heading so that it can
	// be sorted.
	public void headingClicked(MultiColumn list, int col)
	{
	if (col == 0)
		return;	// ignore click on icon column?
	if (col == list.sortcol) {
		list.sortingArrow(col, list.sortdir == 2 ? 1 : 2);
		}
	else {
		list.sortingArrow(col, 1);
		}

	// Re-show the list in the new order, but with the same files selected
	int ss[] = files.allSelected();
	RemoteFile ssf[] = new RemoteFile[ss.length];
	for(int i=0; i<ss.length; i++)
		ssf[i] = showing_list[ss[i]-1];
	show_files(showing_files);
	for(int i=0; i<ss.length; i++) {
		for(int j=0; j<showing_list.length; j++) {
			if (showing_list[j] == ssf[i]) {
				ss[i] = j+1;
				break;
				}
			}
		}
	files.select(ss);
	}

	void open_file_window(RemoteFile f, boolean download, int format)
	{
	try {
		String ext = format == 1 ? ".zip" :
			     format == 2 ? ".tgz" :
			     format == 3 ? ".tar" : "";
		String urlstr;
		if (download) {
			urlstr = "show.cgi"+urlize(f.path)+ext+
				 "?rand="+System.currentTimeMillis()+
				 "&type=application%2Funknown"+
				 "&trust="+trust+
				 "&format="+format+
				 extra;
			}
		else {
			urlstr = "show.cgi"+urlize(f.path)+ext+
				 "?rand="+System.currentTimeMillis()+
				 "&trust="+trust+
				 "&format="+format+
				 extra;
			}

		// Do a test fetch
		String l[] = get_text(urlstr+"&test=1");
		if (l[0].length() > 0) {
			new ErrorWindow(text("eopen", l[0]));
			return;
			}

		// Open for real
		if (download) {
			getAppletContext().showDocument(
				new URL(getDocumentBase(), urlstr));
			}
		else {
			getAppletContext().showDocument(
				new URL(getDocumentBase(), urlstr), "show");
			}
		}
	catch(Exception e) { }
	}

	static String urlize(String s)
	{
	StringBuffer rv = new StringBuffer();
	for(int i=0; i<s.length(); i++) {
		char c = s.charAt(i);
		if (c < 16)
			rv.append("%0"+Integer.toString(c, 16));
		else if ((!Character.isLetterOrDigit(c) && c != '/' &&
		    c != '.' && c != '_' && c != '-') || c >= 128)
			rv.append("%"+Integer.toString(c, 16));
		else
			rv.append(c);
		}
	return rv.toString();
	}

	static String un_urlize(String s)
	{
	StringBuffer rv = new StringBuffer();
	for(int i=0; i<s.length(); i++) {
		char c = s.charAt(i);
		if (c == '%') {
			rv.append((char)Integer.parseInt(
				s.substring(i+1, i+3), 16));
			i += 2;
			}
		else
			rv.append(c);
		}
	return rv.toString();
	}

	// Called back by Javascript when a file or directory has been modified
	public void upload_notify(String path_str, String info)
	{
	int sl = path_str.lastIndexOf('/');
	String par_str = path_str.substring(0, sl),
	       file_str = path_str.substring(sl+1);
	RemoteFile par = find_directory(par_str, false);
	RemoteFile upfile = par.find(file_str);
	try {
		if (upfile == null) {
			// Need to add this file/directory
			upfile = new RemoteFile(this, info, par);
			par.add(upfile);
			}
		else if (upfile.type == RemoteFile.DIR) {
			// Is a directory .. refresh from server
			FileNode upnode = (FileNode)nodemap.get(upfile);
			if (upnode != null)
				upnode.refresh();
			}
		show_files(showing_files);
		}
	catch(Exception e) {
		// In some cases, any attempt to make an HTTP request to
		// refresh the directory may fail because Java apparently has
		// some security rules that limit what a function called from
		// JavaScript is allowed to do. All we can do is ignore the
		// exception :-(
		e.printStackTrace();
		}
	}

	// Called back by Javascript to show an upload-related error
	public void upload_error(String err)
	{
	new ErrorWindow(err);
	}

	public String text(String k, String p[])
	{
	String rv = (String)lang.get(k);
	if (rv == null) rv = "???";
	for(int i=0; i<p.length; i++) {
		int idx = rv.indexOf("$"+(i+1));
		if (idx != -1)
			rv = rv.substring(0, idx)+p[i]+rv.substring(idx+2);
		}
	return rv;
	}

	public String text(String k)
	{
	String p[] = { };
	return text(k, p);
	}

	public String text(String k, String p1)
	{
	String p[] = { p1 };
	return text(k, p);
	}

	public String text(String k, String p1, String p2)
	{
	String p[] = { p1, p2 };
	return text(k, p);
	}

	RemoteFile paste_file(RemoteFile src, RemoteFile dir,
			      String dest, RemoteFile already, boolean mode)
	{
	// Move or copy the actual file
	String[] rv = get_text((mode ? "move.cgi" : "copy.cgi")+
			       "?from="+urlize(src.path)+
			       "&to="+urlize(dest));
	if (rv[0].length() > 0) {
		new ErrorWindow(text(
			mode ? "paste_emfailed" : "paste_ecfailed", rv[0]));
		return null;
		}
	RemoteFile file = new RemoteFile(this, rv[1], dir);
	if (already == null) {
		// Add to the parent directory
		dir.add(file);
		}
	else {
		// Update the existing file
		already.type = file.type;
		already.user = file.user;
		already.group = file.group;
		already.size = file.size;
		already.perms = file.perms;
		already.modified = file.modified;
		file = already;
		}
	if (mode) {
		// Delete the old file
		src.directory.delete(src);
		}
	if (src.type == 0) {
		// Moving or copying a directory.. update the tree
		FileNode dest_par_node =
			(FileNode)nodemap.get(showing_files);
		dest_par_node.add(new FileNode(file));
		if (mode) {
			FileNode cut_par_node =
				(FileNode)nodemap.get(src.directory);
			FileNode cut_file_node =
				(FileNode)nodemap.get(src);
			if (cut_par_node != null &&
			    cut_file_node != null)
				cut_par_node.ch.removeElement(
							cut_file_node);
			}
		dirs.redraw();
		}
	show_files(showing_files);
	return file;
	}

	// Loads the list of filesystems from the server, and refreshes all
	// caches
	void get_filesystems()
	{
	String f[] = get_text("filesystems.cgi");
	got_filesystems = f[0].equals("1");
	acl_support = false;
	attr_support = false;
	ext_support = false;
	mounts.clear();
	fslist.removeAllElements();
	if (got_filesystems) {
		for(int i=1; i<f.length; i++) {
			FileSystem fs = new FileSystem(f[i]);
			fslist.addElement(fs);
			if (fs.acls) acl_support = true;
			if (fs.attrs) attr_support = true;
			if (fs.ext) ext_support = true;
			mounts.put(fs.mount, fs);
			}
		}
	}

	String join_array(String l[])
	{
	String rv = "";
	for(int i=0; i<l.length; i++)
		rv += l[i]+"\n";
	return rv;
	}

	static String replace_str(String str, String os, String ns)
	{
	String rv;
	int idx;
	int pos = 0;
	rv = str;
	while((idx = rv.indexOf(os, pos)) >= 0) {
		rv = rv.substring(0, idx)+
		      ns+rv.substring(idx+os.length());
		pos = idx+ns.length()+1;
		}
	return rv;
	}
}

// A node in the directory tree
class FileNode extends HierarchyNode
{
	FileManager parent;
	RemoteFile file;
	boolean known;

	FileNode(RemoteFile file)
	{
	this.file = file;
	parent = file.parent;
	setimage();
	ch = new Vector();
	text = file.name;
	parent.nodemap.put(file, this);
	}

	// Create the nodes for subdirectories
	void fill()
	{
	if (!known) {
		RemoteFile l[] = file.list();
		if (l == null) return;
		ch.removeAllElements();
		for(int i=0; i<l.length; i++)
			if (l[i].type == 0)
				ch.addElement(new FileNode(l[i]));
		parent.dirs.redraw();
		known = true;
		}
	}

	void add(FileNode n)
	{
	for(int i=0; i<=ch.size(); i++) {
		FileNode ni = i==ch.size() ? null : (FileNode)ch.elementAt(i);
		if (ni == null || ni.text.compareTo(n.text) > 0) {
			ch.insertElementAt(n, i);
			break;
			}
		}
	}

	void setimage()
	{
	im = parent.get_image(file.shared() && file.mounted() ? "smdir.gif" :
			      file.shared() && file.mountpoint() ? "sudir.gif" :
			      file.shared() ? "sdir.gif" :
			      file.mounted() ? "mdir.gif" :
			      file.mountpoint() ? "udir.gif" :
						  "dir.gif");
	}

	// Forces a re-load from the server
	void refresh()
	{
	known = false;
	file.list = null;
	fill();
	}
}

class RemoteFile
{
	static final int DIR = 0;
	static final int TEXT = 1;
	static final int IMAGE = 2;
	static final int BINARY = 3;
	static final int UNKNOWN = 4;
	static final int SYMLINK = 5;
	static final int DEVICE = 6;
	static final int PIPE = 7;
	static final String[] tmap = { "dir.gif", "text.gif", "image.gif",
				       "binary.gif", "unknown.gif",
				       "symlink.gif", "device.gif",
				       "pipe.gif" };

	FileManager parent;
	String path, name;
	int type;
	String user, group;
	long size;
	int perms;
	long modified;
	String linkto;
	RemoteFile list[];
	RemoteFile directory;

	// Parse a line of text to a file object
	RemoteFile(FileManager parent, String line, RemoteFile d)
	{
	this.parent = parent;
	StringTokenizer tok = new StringTokenizer(line, "\t");
	if (tok.countTokens() < 7) {
		String err = "Invalid file line : "+line;
		new ErrorWindow(err);
		throw new Error(err);
		}
	path = tok.nextToken();
	path = parent.replace_str(path, "\\t", "\t");
	path = parent.replace_str(path, "\\\\", "\\");
	type = Integer.parseInt(tok.nextToken());
	user = tok.nextToken();
	group = tok.nextToken();
	size = Long.parseLong(tok.nextToken());
	perms = Integer.parseInt(tok.nextToken());
	modified = Long.parseLong(tok.nextToken())*1000;
	if (type == 5) linkto = tok.nextToken();
	directory = d;
	if (path.equals("/")) name = "/";
	else name = path.substring(path.lastIndexOf('/')+1);
	}

	// Create a new, empty file object
	RemoteFile() { }

	// Returns a list of files in this directory
	RemoteFile[] list()
	{
	if (list == null) {
		String l[] = parent.get_text("list.cgi?dir="+
					     parent.urlize(path));
		if (l[0].length() > 0) {
			//list = new RemoteFile[0];
			// Error reading the remote directory!
			new ErrorWindow(parent.text("list_edir", path, l[0]));
			list = null;
			}
		else {
			list = new RemoteFile[l.length-3];
			for(int i=3; i<l.length; i++)
				list[i-3] = new RemoteFile(parent, l[i], this);
			}
		}
	return list;
	}

	RemoteFile find(String n)
	{
	RemoteFile l[] = list();
	if (l != null) {
		for(int i=0; i<l.length; i++)
			if (l[i].name.equals(n))
				return l[i];
		}
	return null;
	}

	void add(RemoteFile f)
	{
	RemoteFile nlist[] = new RemoteFile[list.length+1];
	int offset = 0;
	for(int i=0; i<list.length; i++) {
		if (list[i].name.compareTo(f.name) > 0 && offset == 0) {
			nlist[i] = f;
			offset++;
			}
		nlist[i+offset] = list[i];
		}
	if (offset == 0) nlist[list.length] = f;
	list = nlist;
	}

	void delete(RemoteFile f)
	{
	RemoteFile nlist[] = new RemoteFile[list.length-1];
	for(int i=0,j=0; i<list.length; i++)
		if (list[i] != f)
			nlist[j++] = list[i];
	list = nlist;
	}

	boolean shared()
	{
	return type == DIR &&
	       (parent.stab.get(path) != null ||
	        parent.ntab.get(path) != null);
	}

	boolean mountpoint()
	{
	return type == DIR && fs() != null;
	}

	boolean mounted()
	{
	FileSystem fs = fs();
	return type == DIR && fs != null && fs.mtab;
	}

	FileSystem fs()
	{
	return (FileSystem)parent.mounts.get(path);
	}

}

class EditorWindow extends FixedFrame implements CbButtonCallback
{
	TextField name;
	TextArea edit;
	CbButton save_b, saveclose_b, cancel_b, goto_b, find_b;
	Checkbox dosmode;
	RemoteFile file;
	FileManager filemgr;
	GotoWindow goto_window;
	FindReplaceWindow find_window;
	String charset;

	// Editing an existing file
	EditorWindow(RemoteFile f, FileManager p)
	{
	super(800, 600);
	file = f; filemgr = p;
	makeUI(false);
	setTitle(filemgr.text("edit_title", file.path));

	// Load the file
	try {
		URL u = new URL(filemgr.getDocumentBase(),
				"show.cgi"+filemgr.urlize(file.path)+
				"?rand="+System.currentTimeMillis()+
				"&trust="+filemgr.trust+"&edit=1"+
				filemgr.extra);
		URLConnection uc = u.openConnection();
		filemgr.set_cookie(uc);
		int len = uc.getContentLength();
		InputStream is = uc.getInputStream();
		charset = filemgr.get_charset(uc.getContentType());
		byte buf[];
		if (len >= 0) {
			// Length is known
			buf = new byte[uc.getContentLength()];
			int got = 0;
			while(got < buf.length)
				got += is.read(buf, got, buf.length-got);
			}
		else {
			// Length is unknown .. read till the end
			buf = new byte[0];
			while(true) {
			    byte data[] = new byte[16384];
			    int got;
			    try { got = is.read(data); }
			    catch(EOFException ex) { break; }
			    if (got <= 0) break;
			    byte nbuf[] = new byte[buf.length + got];
			    System.arraycopy(buf, 0, nbuf, 0, buf.length);
			    System.arraycopy(data, 0, nbuf, buf.length, got);
			    buf = nbuf;
			    }
			}
		String s = charset == null ? new String(buf, 0)
					   : new String(buf, charset);
		if (s.indexOf("\r\n") != -1) {
			dosmode.setState(true);
			s = FileManager.replace_str(s, "\r\n", "\n");
			}
		edit.setText(s);
		is.close();
		file.size = buf.length;
		}
	catch(Exception e) { e.printStackTrace(); }
	}

	// Creating a new file
	EditorWindow(String f, FileManager p)
	{
	super(800, 600);
	filemgr = p;
	makeUI(true);
	setTitle(filemgr.text("edit_title2"));
	name.setText(f.equals("/") ? f : f+"/");
	name.select(name.getText().length(), name.getText().length());
	}

	void makeUI(boolean add_name)
	{
	setLayout(new BorderLayout());
	if (add_name) {
		Panel np = new Panel();
		np.setLayout(new BorderLayout());
		np.add("West", new Label(filemgr.text("edit_filename")));
		np.add("Center", name = new TextField());
		name.setFont(filemgr.fixed);
		add("North", np);
		}
	add("Center", edit = new TextArea(20, 80));
	edit.setEditable(true);
	edit.setFont(filemgr.fixed);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(dosmode = new Checkbox("Windows newlines"));
	bot.add(goto_b = new CbButton(filemgr.get_image("goto.gif"),
				      filemgr.text("edit_goto"),
				      CbButton.LEFT, this));
	bot.add(find_b = new CbButton(filemgr.get_image("find.gif"),
				      filemgr.text("edit_find"),
				      CbButton.LEFT, this));
	bot.add(new Label(" "));
	bot.add(save_b = new CbButton(filemgr.get_image("save.gif"),
				      filemgr.text("save"),
				      CbButton.LEFT, this));
	bot.add(saveclose_b = new CbButton(filemgr.get_image("save.gif"),
				      filemgr.text("edit_saveclose"),
				      CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("close"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == save_b || b == saveclose_b) {
		RemoteFile par = null, already = null;
		String save_path;
		if (file == null) {
			// Locate the filemgr directory
			save_path = filemgr.trim_path(name.getText());
			int sl = save_path.lastIndexOf('/');
			par = filemgr.find_directory(
					save_path.substring(0, sl), false);
			if (par == null) return;
			already = par.find(save_path.substring(sl+1));
			if (already != null &&
			    (already.type == 0 || already.type == 5)) {
				new ErrorWindow(
					filemgr.text("edit_eover", save_path));
				return;
				}
			}
		else save_path = file.path;

		// Save the file back again
		String s = edit.getText(), line;
		s = FileManager.replace_str(s, "\r\n", "\n");
		try {
			if (dosmode.getState()) {
				// Convert to DOS newlines
				s = FileManager.replace_str(s, "\n", "\r\n");
				}
			else {
				// Remove any DOS newlines
				s = FileManager.replace_str(s, "\r\n", "\n");
				}
			URL u = new URL(filemgr.getDocumentBase(),
					"save.cgi"+filemgr.urlize(save_path)+
					"?rand="+System.currentTimeMillis()+
					"&trust="+filemgr.trust+
					"&length="+s.length()+
					filemgr.extra);
			URLConnection uc = u.openConnection();
			uc.setRequestProperty("Content-type", "text/plain");
			filemgr.set_cookie(uc);
			uc.setDoOutput(true);
			OutputStream os = uc.getOutputStream();
			byte buf[];
			if (charset == null) {
				// Assume ascii
				buf = new byte[s.length()];
				s.getBytes(0, buf.length, buf, 0);
				}
			else {
				// Convert back to original charset
				buf = s.getBytes(charset);
				}
			os.write(buf);
			os.close();
			BufferedReader is =
			    new BufferedReader(new InputStreamReader(
				uc.getInputStream()));
			String err = is.readLine();
			if (err.length() > 0) {
				new ErrorWindow(
					filemgr.text("edit_esave", err));
				is.close();
				return;
				}
			line = is.readLine();
			is.close();
			}
		catch(Exception e) { e.printStackTrace(); return; }

		if (file == null) {
			// Create and insert or replace the file object
			file = new RemoteFile(filemgr, line, par);
			if (already != null) {
				// A file with this name exists
				already.type = file.type;
				already.user = file.user;
				already.group = file.group;
				already.size = file.size;
				already.perms = file.perms;
				already.modified = file.modified;
				}
			else {
				// Add to the list
				par.add(file);
				}
			}
		else {
			file.size = s.length();
			file.modified = System.currentTimeMillis();
			}
		filemgr.show_files(filemgr.showing_files);
		if (b == saveclose_b)
			dispose();
		}
	else if (b == cancel_b) {
		// Just close
		dispose();
		}
	else if (b == goto_b) {
		// Open a dialog asking which line to go to
		if (goto_window != null)
			goto_window.toFront();
		else
			goto_window = new GotoWindow(this);
		}
	else if (b == find_b) {
		// Open the search (and replace) dialog
		if (find_window != null)
			find_window.toFront();
		else
			find_window = new FindReplaceWindow(this);
		}
	}

	public void dispose()
	{
	super.dispose();
	if (goto_window != null) goto_window.dispose();
	if (find_window != null) find_window.dispose();
	}
}

class GotoWindow extends FixedFrame implements CbButtonCallback
{
	EditorWindow editor;
	FileManager filemgr;
	TextField line;
	CbButton goto_b, cancel_b;

	GotoWindow(EditorWindow e)
	{
	editor = e;
	filemgr = e.filemgr;

	setLayout(new BorderLayout());
	add("West", new Label(filemgr.text("edit_gotoline")));
	add("Center", line = new TextField(10));
	line.setFont(filemgr.fixed);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(goto_b = new CbButton(filemgr.get_image("goto.gif"),
				      filemgr.text("edit_goto"),
				      CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
				        filemgr.text("close"),
				        CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == goto_b) {
		// Go to the chose line, if it exists
		int lnum;
		try { lnum = Integer.parseInt(line.getText()); }
		catch(Exception e) { return; }

		String txt = editor.edit.getText();
		int c, l = 0;
		for(c=0; c<txt.length(); c++) {
			if (txt.charAt(c) == '\n') {
				l++;
				if (l == lnum) {
					// Found the line!
					editor.edit.select(c, c);
					dispose();
					editor.edit.requestFocus();
					return;
					}
				}
			}
		}
	else if (b == cancel_b) {
		// Just close the window
		dispose();
		}
	}

	public void dispose()
	{
	super.dispose();
	editor.goto_window = null;
	}

	public boolean handleEvent(Event e)
	{
	if (e.target == line && e.id == Event.KEY_RELEASE && e.key == 10) {
		click(goto_b);
		return true;
		}
	return false;
	}
}

class FindReplaceWindow extends FixedFrame implements CbButtonCallback
{
	EditorWindow editor;
	FileManager filemgr;
	TextField find, replace;
	CbButton find_b, replace_b, all_b, cancel_b;

	FindReplaceWindow(EditorWindow e)
	{
	editor = e;
	filemgr = e.filemgr;
	setLayout(new BorderLayout());

	Panel left = new Panel();
	left.setLayout(new GridLayout(2, 1));
	left.add(new Label(filemgr.text("edit_searchfor")));
	left.add(new Label(filemgr.text("edit_replaceby")));
	add("West", left);

	Panel right = new Panel();
	right.setLayout(new GridLayout(2, 1));
	right.add(find = new TextField(40));
	find.setFont(filemgr.fixed);
	right.add(replace = new TextField(40));
	replace.setFont(filemgr.fixed);
	add("Center", right);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(find_b = new CbButton(filemgr.get_image("find.gif"),
				      filemgr.text("edit_find"),
				      CbButton.LEFT, this));
	bot.add(replace_b = new CbButton(filemgr.get_image("replace.gif"),
				      filemgr.text("edit_replace"),
				      CbButton.LEFT, this));
	bot.add(all_b = new CbButton(filemgr.get_image("all.gif"),
				      filemgr.text("edit_all"),
				      CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
				        filemgr.text("close"),
				        CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	String findtxt = find.getText();
	String edittxt = editor.edit.getText();
	if (findtxt.length() == 0)
		return;
	if (b == find_b) {
		// Find the next occurrance of the text, starting from
		// the cursor + 1, and select it
		int pos = edittxt.indexOf(findtxt,
					   editor.edit.getSelectionStart()+1);
		if (pos < 0) {
			// Not found .. but try wrap-around
			pos = edittxt.indexOf(findtxt, 0);
			}
		if (pos < 0)
			new ErrorWindow(filemgr.text("edit_notfound", findtxt));
		else {
			editor.edit.select(pos, pos+findtxt.length());
			editor.edit.requestFocus();
			}
		}
	else if (b == replace_b) {
		// If the word to search for is selected, replace it. Otherwise
		// just search for the next one
		int st = editor.edit.getSelectionStart(),
		    en = editor.edit.getSelectionEnd();
		if (st >= 0) {
			String sel = edittxt.substring(st, en);
			if (sel.equals(findtxt)) {
				// Replace the selected
				editor.edit.setText(edittxt.substring(0, st)+
						    replace.getText()+
						    edittxt.substring(en));
				editor.edit.select(st, st);
				return;
				}
			}
		click(find_b);
		}
	else if (b == all_b) {
		// Replace all occurrances of the text in the editor
		int pos = 0;
		int len = findtxt.length();
		int st = editor.edit.getSelectionStart(),
		    en = editor.edit.getSelectionEnd();
		while((pos = edittxt.indexOf(findtxt, pos)) != -1) {
			edittxt = edittxt.substring(0, pos)+
				  replace.getText()+
				  edittxt.substring(pos+len);
			pos += len;
			}
		editor.edit.setText(edittxt);
		editor.edit.select(st, en);	// put back old selection
		}
	else if (b == cancel_b) {
		// Just close the window
		dispose();
		}
	}

	public void dispose()
	{
	super.dispose();
	editor.find_window = null;
	}
}

class PropertiesWindow extends FixedFrame implements CbButtonCallback
{
	RemoteFile file;
	FileManager filemgr;
	CbButton save_b, cancel_b, size_b;

	TextField linkto;
	TextField user, group;
	Checkbox setuid, setgid;
	PermissionsPanel user_p, group_p, other_p;
	Checkbox sticky;
	Choice rec_mode;
	TextField octal;

	TextField bytes, files, dirs;

	PropertiesWindow(RemoteFile f, FileManager p)
	{
	file = f;
	filemgr = p;

	// Create UI
	setTitle(f.path);
	setLayout(new BorderLayout());
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	if (file.type == 0) {
		bot.add(size_b = new CbButton(filemgr.get_image("refresh.gif"),
					      filemgr.text("info_getsize"),
					      CbButton.LEFT, this));
		}
	if (filemgr.can_perms || filemgr.can_users) {
		bot.add(save_b = new CbButton(filemgr.get_image("save.gif"),
					      filemgr.text("save"),
					      CbButton.LEFT, this));
		}
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);

	Panel mid = new Panel();
	mid.setLayout(new BorderLayout());
	TabbedPanel tab = null;
	add("Center", mid);

	// Create file details section
	Panel det = new LinedPanel(filemgr.text("info_file")),
	      dl = new Panel(), dr = new Panel();
	setup_leftright(det, dl, dr);
	add_item(filemgr.text("info_path"),
		new Label(file.path), dl, dr);
	add_item(filemgr.text("info_type"),
		new Label(filemgr.text("file_type"+file.type)), dl, dr);
	add_item(filemgr.text("info_size"),
		new Label(String.valueOf(file.size)),dl,dr);
	add_item(filemgr.text("info_mod"),
		new Label(String.valueOf(new Date(file.modified))), dl, dr);
	if (file.type == 5) {
		add_item(filemgr.text("info_link"),
			 linkto = new TextField(file.linkto, 30), dl, dr);
		linkto.setFont(filemgr.fixed);
		}
	mid = add_panel(mid, det);

	if (filemgr.can_perms) {
		// Create permissions section
		Panel per = new LinedPanel(filemgr.text("info_perms")),
		      pl = new Panel(), pr = new Panel();
		setup_leftright(per, pl, pr);
		add_item(filemgr.text("info_user"),
		    user_p = new PermissionsPanel(file, 64, filemgr), pl, pr);
		add_item(filemgr.text("info_group"),
		    group_p = new PermissionsPanel(file, 8, filemgr), pl, pr);
		add_item(filemgr.text("info_other"),
		    other_p = new PermissionsPanel(file, 1, filemgr), pl,pr);
		if (file.type == 0) {
			add_item(filemgr.text("info_sticky"),
			    sticky = new Checkbox(filemgr.text("info_sticky2")),
			    pl,pr);
			sticky.setState((file.perms&01000) != 0);
			}
		add_item(filemgr.text("info_octal"),
			 octal = new TextField(4), pl, pr);
		octal.setFont(filemgr.fixed);
		octal.setEditable(false);
		mid = add_panel(mid, per);
		}

	if (filemgr.can_users) {
		// Create ownership section
		Panel own = new LinedPanel(filemgr.text("info_own")),
		      ol = new Panel(), or = new Panel();
		setup_leftright(own, ol, or);
		add_item(filemgr.text("info_user"),
			 user = new TextField(file.user, 10), ol, or);
		user.setFont(filemgr.fixed);
		if (file.type != 0) {
			add_item(filemgr.text("info_setuid"),
			    setuid = new Checkbox(filemgr.text("info_setuid2")),
			    ol, or);
			setuid.setState((file.perms & 0x800) != 0);
			}
		add_item(filemgr.text("info_group"),
			 group = new TextField(file.group, 10), ol, or);
		group.setFont(filemgr.fixed);
		if (file.type == 0)
			add_item(filemgr.text("info_setgid"),
			  setgid = new Checkbox(filemgr.text("info_setgid2")),
			  ol, or);
		else
			add_item(filemgr.text("info_setgid"),
			  setgid = new Checkbox(filemgr.text("info_setgid3")),
			  ol, or);
		setgid.setState((file.perms & 0x400) != 0);
		mid = add_panel(mid, own);
		}

	if (file.type == 0) {
		// Create directory size section, initially empty
		Panel szp = new LinedPanel(filemgr.text("info_sizeheader")),
		      sl = new Panel(), sr = new Panel();
		setup_leftright(szp, sl, sr);
		add_item(filemgr.text("info_bytes"),
			 bytes = new TextField("", 10), sl, sr);
		bytes.setFont(filemgr.fixed);
		bytes.setEditable(false);
		add_item(filemgr.text("info_files"),
			 files = new TextField("", 10), sl, sr);
		files.setFont(filemgr.fixed);
		files.setEditable(false);
		add_item(filemgr.text("info_dirs"),
			 dirs = new TextField("", 10), sl, sr);
		dirs.setFont(filemgr.fixed);
		dirs.setEditable(false);
		mid = add_panel(mid, szp);
		}

	if (file.type == 0 && (filemgr.can_perms || filemgr.can_users)) {
		// Create recursion section
		Panel rec = new LinedPanel(filemgr.text("info_apply"));
		rec.setLayout(new BorderLayout());
		rec_mode = new Choice();
		for(int i=1; i<=5; i++)
			rec_mode.addItem(filemgr.text("info_apply"+i));
		rec.add("Center", rec_mode);
		mid = add_panel(mid, rec);
		}

	set_octal();
	Util.recursiveBody(this);
	pack();
	show();
	}

	Panel add_panel(Panel p, Component c)
	{
	p.add("North", c);
	Panel np = new Panel();
	np.setLayout(new BorderLayout());
	p.add("Center", np);
	return np;
	}

	public void click(CbButton b)
	{
	if (b == save_b) {
		// Update the file
		int perms = get_perms();
		String user_str = user != null ? user.getText() : null;
		String group_str = group != null ? group.getText() : null;
		int rec = 0;
		if (file.type == 0 && rec_mode != null)
			rec = rec_mode.getSelectedIndex();
		String rv[] = filemgr.get_text(
			"chmod.cgi?path="+filemgr.urlize(file.path)+
			(perms < 0 ? "" : "&perms="+perms)+
			(user_str == null ? "" :
				"&user="+filemgr.urlize(user_str))+
			(group_str == null ? "" :
				"&group="+filemgr.urlize(group_str))+
			"&rec="+rec+
			(linkto==null ? "" :
				"&linkto="+filemgr.urlize(linkto.getText())));
		if (rv[0].length() > 0) {
			// Something went wrong
			new ErrorWindow(filemgr.text("info_efailed",
					file.path, rv[0]));
			}
		else {
			// Update all changed file objects
			if (linkto != null)
				file.linkto = linkto.getText();
			else if (rec == 0) {
				// This file or directory only
				update_file(file, perms, false);
				}
			else if (rec == 1) {
				// Update files in this directory
				update_file(file, perms, false);
				recurse_files(file, perms, false, false, true);
				}
			else if (rec == 2) {
				// Update files and subdirs
                                update_file(file, perms, false);
				recurse_files(file, perms, true, true, true);
				}
			else if (rec == 3) {
				// Update files only in dir and subdirs
				recurse_files(file, perms, true, false, true);
				}
			else if (rec == 4) {
				// Update dir and subdirs but not files
				recurse_files(file, perms, true, true, false);
				}

			// Update directory list
			int os = filemgr.files.selected();
			filemgr.show_files(filemgr.showing_files);
			filemgr.files.select(os);
			dispose();
			}
		}
	else if (b == size_b) {
		// Get the size of the directory recursively
		String l[] = filemgr.get_text("size.cgi?dir="+
					      filemgr.urlize(file.path));
		if (l[0].length() > 0) {
			new ErrorWindow(filemgr.text("info_size", l[0]));
			}
		StringTokenizer tok = new StringTokenizer(l[1], " ");
		String bytes_str = tok.nextToken();
		files.setText(tok.nextToken());
		dirs.setText(tok.nextToken());
		bytes.setText(tok.nextToken()+" "+tok.nextToken());
		}
	else {
		// Just close
		dispose();
		}
	}

	void update_file(RemoteFile f, int perms, boolean perms_only)
	{
	f.user = user.getText();
	f.group = group.getText();
	if (perms_only)
		f.perms = (perms & 0777) | (f.perms & 037777777000);
	else
		f.perms = perms;
	}

	void recurse_files(RemoteFile f, int perms, boolean do_subs,
			   boolean do_dirs, boolean do_files)
	{
	if (f.list == null) return;
	for(int i=0; i<f.list.length; i++) {
		RemoteFile ff = f.list[i];
		if (ff.type == 5) continue;
		else if (ff.type == 0) {
			if (do_subs) {
				if (do_dirs) {
					update_file(ff, perms, false);
					}
				recurse_files(ff, perms, true, do_dirs, do_files);
				}
			}
		else {
			if (do_files) {
				update_file(ff, perms, true);
				}
			}
		}
	}

	void setup_leftright(Panel m, Panel l, Panel r)
	{
	m.setLayout(new BorderLayout());
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", l);
	p.add("Center", r);
	l.setLayout(new GridLayout(0, 1));
	r.setLayout(new GridLayout(0, 1));
	m.add("North", p);
	}

	void add_item(String t, Component c, Panel l, Panel r)
	{
	l.add(new Label(t));
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", c);
	r.add(p);
	}

	void set_octal()
	{
	if (octal != null) {
		String oct = Integer.toOctalString(get_perms());
		while(oct.length() < 4)
			oct = "0"+oct;
		octal.setText(oct);
		}
	}

	int get_perms()
	{
	if (user_p == null)
		return -1;		// Cannot edit
	int perms = 0;
	if (setuid == null)
		perms |= (file.perms & 0x800);
	else
		perms |= (setuid.getState() ? 0x800 : 0);
	perms |= (setgid.getState() ? 0x400 : 0);
	perms |= user_p.getPerms();
	perms |= group_p.getPerms();
	perms |= other_p.getPerms();
	if (sticky == null)
		perms |= (file.perms & 01000);
	else
		perms |= (sticky.getState() ? 01000 : 0);
	return perms;
	}

	public boolean handleEvent(Event e)
	{
	if (e.target instanceof Checkbox) {
		set_octal();
		return true;
		}
	return super.handleEvent(e);
	}
}

class PermissionsPanel extends Panel
{
	Checkbox read, write, exec;
	int base;

	PermissionsPanel(RemoteFile file, int base, FileManager filemgr)
	{
	int perms = file.perms;
	this.base = base;
	setLayout(new GridLayout(1, 3));
	add(read = new Checkbox(filemgr.text("info_read")));
	read.setState((perms&(base<<2)) != 0);
	add(write = new Checkbox(filemgr.text("info_write")));
	write.setState((perms&(base<<1)) != 0);
	add(exec = new Checkbox(
		filemgr.text(file.type == RemoteFile.DIR ? "info_list"
							 : "info_exec")));
	exec.setState((perms&base) != 0);
	}

	int getPerms()
	{
	int rv = 0;
	rv |= (read.getState() ? (base<<2) : 0);
	rv |= (write.getState() ? (base<<1) : 0);
	rv |= (exec.getState() ? base : 0);
	return rv;
	}
}

class DeleteWindow extends FixedFrame implements CbButtonCallback
{
	CbButton delete_b, cancel_b;
	FileManager filemgr;
	RemoteFile files[];

	DeleteWindow(FileManager p, RemoteFile ff[])
	{
	filemgr = p;
	files = ff;
	setTitle(filemgr.text(ff.length > 1 ? "delete_mtitle" :
			      ff[0].type == 0 ? "delete_dtitle" :
						"delete_ftitle"));

	setLayout(new BorderLayout());
	if (ff.length > 1) {
		add("North", new Label(filemgr.text("delete_mdesc")));
		Panel mp = new Panel();
		mp.setLayout(new GridLayout(ff.length, 1));
		for(int i=0; i<ff.length; i++)
			mp.add(new Label(ff[i].path));
		add("Center", mp);
		}
	else
		add("Center", new MultiLabel(filemgr.text(
			ff[0].type == 0 ? "delete_ddesc" : "delete_fdesc",
			ff[0].path), 35));
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(delete_b = new CbButton(filemgr.get_image("save.gif"),
				        filemgr.text("delete"),
					CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == delete_b) {
		// Delete the file or directory
		boolean need_redraw = false, need_reshow = false;
		for(int i=0; i<files.length; i++) {
			RemoteFile file = files[i];
			String rv[] = filemgr.get_text("delete.cgi?file="+
					       filemgr.urlize(file.path));
			if (rv[0].length() > 0) {
				new ErrorWindow(filemgr.text("delete_efailed",
						file.path, rv[0]));
				break;
				}
			else {
				// done the deed.. update data structures
				RemoteFile pf = file.directory;
				pf.delete(file);
				if (filemgr.showing_files == pf) {
					// Need to refresh the list as well..
					need_reshow = true;
					}

				FileNode node = (FileNode)filemgr.nodemap.get(
							file);
				FileNode pnode = (FileNode)filemgr.nodemap.get(
							pf);
				if (node != null) {
					// Take the directory out of the tree..
					pnode.ch.removeElement(node);
					need_redraw = true;
					}
				}
			}
		if (need_reshow) filemgr.show_files(filemgr.showing_files);
		if (need_redraw) filemgr.dirs.redraw();
		dispose();
		}
	else if (b == cancel_b)
		dispose();
	}
}

class MkdirWindow extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	TextField dir;
	CbButton create_b, cancel_b;

	MkdirWindow(String d, FileManager p)
	{
	filemgr = p;
	setTitle(filemgr.text("mkdir_title"));
	setLayout(new BorderLayout());
	add("West", new Label(filemgr.text("mkdir_dir")));
	add("Center", dir = new TextField(d.equals("/") ? "/" : d+"/", 40));
	dir.setFont(filemgr.fixed);
	dir.select(dir.getText().length(), dir.getText().length());
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(create_b = new CbButton(filemgr.get_image("save.gif"),
				        filemgr.text("create"),
					CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == create_b) {
		// Find the filemgr directory
		String path = dir.getText();
		path = filemgr.trim_path(path);
		int sl = path.lastIndexOf('/');
		RemoteFile par = filemgr.find_directory(
					path.substring(0, sl), false);
		if (par.find(path.substring(sl+1)) != null) {
			new ErrorWindow(filemgr.text("mkdir_eexists", path));
			return;
			}
		String rv[] = filemgr.get_text("mkdir.cgi?dir="+
					       filemgr.urlize(path));
		if (rv[0].length() > 0) {
			new ErrorWindow(filemgr.text("mkdir_efailed", rv[0]));
			return;
			}
		RemoteFile file = new RemoteFile(filemgr, rv[1], par);
		par.add(file);
		FileNode parnode = (FileNode)filemgr.nodemap.get(par);
		if (parnode != null) {
			// Update the tree
			parnode.add(new FileNode(file));
			filemgr.dirs.redraw();
			}
		filemgr.show_files(filemgr.showing_files);
		dispose();
		}
	else dispose();
	}
}

class LinkWindow extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	TextField from, to;
	CbButton create_b, cancel_b;

	LinkWindow(String d, FileManager p)
	{
	filemgr = p;
	setLayout(new BorderLayout());
	setTitle(filemgr.text("link_title"));
	Panel l = new Panel(), r = new Panel();
	l.setLayout(new GridLayout(0, 1));
	l.add(new Label(filemgr.text("link_from")));
	l.add(new Label(filemgr.text("link_to")));
	r.setLayout(new GridLayout(0, 1));
	r.add(from = new TextField(d.equals("/") ? "/" : d+"/", 40));
	from.setFont(filemgr.fixed);
	from.select(from.getText().length(), from.getText().length());
	r.add(to = new TextField());
	to.setFont(filemgr.fixed);
	add("West", l); add("Center", r);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(create_b = new CbButton(filemgr.get_image("save.gif"),
				        filemgr.text("create"),
					CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == create_b) {
		// Check inputs
		String from_str = from.getText().trim();
		if (!from_str.startsWith("/")) {
			new ErrorWindow(filemgr.text("link_efrom", from_str));
			return;
			}
		int sl = from_str.lastIndexOf('/');
		String par_str = from_str.substring(0, sl),
		       file_str = from_str.substring(sl+1);
		RemoteFile par = filemgr.find_directory(par_str, false);
		if (par == null) return;
		if (par.find(file_str) != null) {
			new ErrorWindow(filemgr.text("link_eexists", from_str));
			return;
			}

		// Create the actual link
		String rv[] = filemgr.get_text("makelink.cgi?from="+
					       filemgr.urlize(from_str)+"&to="+
					       filemgr.urlize(to.getText()));
		if (rv[0].length() > 0) {
			new ErrorWindow(filemgr.text("link_efailed", rv[0]));
			return;
			}
		RemoteFile file = new RemoteFile(filemgr, rv[1], par);
		par.add(file);
		filemgr.show_files(filemgr.showing_files);
		dispose();
		}
	else if (b == cancel_b)
		dispose();
	}
}

class RenameWindow extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	RemoteFile file;
	TextField oldname, newname;
	CbButton rename_b, cancel_b;

	RenameWindow(FileManager p, RemoteFile f)
	{
	filemgr = p; file = f;
	setLayout(new BorderLayout());
	setTitle(filemgr.text("rename_title", file.path));
	Panel l = new Panel(), r = new Panel();
	l.setLayout(new GridLayout(0, 1));
	l.add(new Label(filemgr.text("rename_old")));
	l.add(new Label(filemgr.text("rename_new")));
	r.setLayout(new GridLayout(0, 1));
	r.add(oldname = new TextField(file.name, 20));
	oldname.setEditable(false);
	oldname.setFont(filemgr.fixed);
	r.add(newname = new TextField(file.name, 20));
	newname.select(file.name.length(), file.name.length());
	newname.setFont(filemgr.fixed);
	add("West", l); add("Center", r);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(rename_b = new CbButton(filemgr.get_image("save.gif"),
				        filemgr.text("rename_ok"),
					CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	pack();
	show();
	Util.recursiveBody(this);
	}

	public void click(CbButton b)
	{
	if (b == rename_b) {
		// Work out destination file and directory
		String newstr = newname.getText().trim();
		if (newstr.length() == 0) return;
		RemoteFile destdir;
		String newpath;
		if (newstr.indexOf('/') >= 0) {
			// Different dir
			if (newstr.startsWith("/")) {
				// Some absolute path
				newpath = newstr;
				}
			else {
				// Relative to this dir
				newpath = file.directory.path+"/"+newstr;
				}
			int sl = newpath.lastIndexOf('/');
			String newdir = sl == 0 ? "/" : newpath.substring(0,sl);
			destdir = filemgr.find_directory(newdir, false);
			}
		else {
			// Same dir
			destdir = file.directory;
			int sl = file.path.lastIndexOf('/');
			newpath = file.path.substring(0, sl)+"/"+newstr;
			}

		// Work out filename only
		int sl = newpath.lastIndexOf('/');
		newstr = newpath.substring(sl+1);

		// Check for an existing file
		RemoteFile already = destdir.find(newstr);
		if (already != null) {
			new ErrorWindow(filemgr.text("rename_eexists", newstr));
			return;
			}

		// Rename the real file
		String rv[] = filemgr.get_text(
				"rename.cgi?old="+filemgr.urlize(file.path)+
				"&new="+filemgr.urlize(newpath));
		if (rv[0].length() > 0) {
			new ErrorWindow(filemgr.text("rename_efailed", rv[0]));
			return;
			}

		// Update data structure
		file.name = newstr;
		file.path = newpath;
		file.directory.delete(file);
		destdir.list();
		destdir.add(file);
		file.directory = destdir;
		file.list = null;
		FileNode parnode = (FileNode)filemgr.nodemap.get(file.directory);
		FileNode filenode = (FileNode)filemgr.nodemap.get(file);
		if (parnode != null && filenode != null) {
			// Need to refresh tree
			filenode.text = file.name;
			parnode.ch.removeElement(filenode);
			parnode.add(filenode);
			dispose();
			filemgr.dirs.redraw();
			}

		filemgr.show_files(filemgr.showing_files);
		dispose();
		}
	else if (b == cancel_b)
		dispose();
	}
}

class OverwriteWindow extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	RemoteFile src, already;
	TextField newname;
	CbButton ok, cancel;
	int idx;
	boolean mode;

	OverwriteWindow(FileManager p, RemoteFile a, RemoteFile s, int i)
	{
	filemgr = p; src = s; already = a; idx = i;
	mode = filemgr.cut_mode;
	setLayout(new BorderLayout());
	setTitle(filemgr.text("over_title"));
	add("North",
	    new MultiLabel(filemgr.text("over_msg", already.path), 30, 0));
	add("West", new Label(filemgr.text("over_new")));
	add("East", newname = new TextField(a.name, 30));
	newname.setFont(filemgr.fixed);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				  filemgr.text("over_ok"),
				  CbButton.LEFT, this));
	bot.add(cancel = new CbButton(filemgr.get_image("cancel.gif"),
				  filemgr.text("cancel"),
				  CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == cancel)
		dispose();
	else if (b == ok && newname.getText().length() > 0) {
		// paste the file, but with a new name
		RemoteFile ap = already.directory;
		RemoteFile newalready = ap.find(newname.getText());
		if (newalready == src) {
			new ErrorWindow(filemgr.text("paste_eself"));
			return;
			}
		if (newalready != null && (newalready.type == 0 ||
					   newalready.type == 5)) {
			new ErrorWindow(
				filemgr.text("paste_eover", newalready.path));
			return;
			}
		String dpath = (ap.path.equals("/") ? "/" :
				ap.path+"/")+newname.getText();
		RemoteFile nf = filemgr.paste_file(src, already.directory,
						   dpath, newalready, mode);
		if (filemgr.cut_mode && nf != null) {
			// Paste from the destination path from now on
			filemgr.cut_buffer[idx] = nf;
			}
		dispose();
		}
	}
}

class SambaShare
{
	String path;
	boolean available;
	boolean writable;
	int guest;
	String comment;

	SambaShare(String l)
	{
	StringSplitter tok = new StringSplitter(l, ':');
	path = tok.nextToken();
	available = tok.nextToken().equals("1");
	writable = tok.nextToken().equals("1");
	guest = Integer.parseInt(tok.nextToken());
	comment = tok.nextToken();
	}

	SambaShare(String p, boolean a, boolean w, int g, String c)
	{
	path = p;
	available = a;
	writable = w;
	guest = g;
	comment = c;
	}

	String params()
	{
	return "path="+FileManager.urlize(path)+
	       "&available="+(available ? 1 : 0)+
	       "&writable="+(writable ? 1 : 0)+
	       "&guest="+guest+
	       "&comment="+FileManager.urlize(comment);
	}
}

class DFSAdminExport
{
	String path;
	String desc;
	String ro, rw, root;

	DFSAdminExport(String l)
	{
	StringSplitter tok = new StringSplitter(l, ':');
	path = tok.nextToken();
	ro = tok.nextToken();
	rw = tok.nextToken();
	root = tok.nextToken();
	desc = tok.nextToken();
	}

	DFSAdminExport(String p, String d, String ro, String rw, String root)
	{
	path = p;
	desc = d;
	this.ro = ro;
	this.rw = rw;
	this.root = root;
	}

	static String[] split(String s)
	{
	StringTokenizer stok = new StringTokenizer(s, " ");
	String rv[] = new String[stok.countTokens()];
	for(int i=0; i<rv.length; i++)
		rv[i] = stok.nextToken();
	return rv;
	}

	String params()
	{
	return "path="+FileManager.urlize(path)+
	       "&ro="+FileManager.urlize(ro)+
	       "&rw="+FileManager.urlize(rw)+
	       "&root="+FileManager.urlize(root)+
	       "&desc="+FileManager.urlize(desc);
	}
}

class LinuxExport
{
	String path;
	String host[];
	boolean ro[];
	int squash[];

	LinuxExport(String l)
	{
	StringSplitter tok = new StringSplitter(l, ':');
	path = tok.nextToken();
	int c = tok.countTokens() / 3;
	host = new String[c];
	ro = new boolean[c];
	squash = new int[c];
	for(int i=0; tok.hasMoreTokens(); i++) {
		host[i] = tok.nextToken();
		ro[i] = tok.nextToken().equals("1");
		squash[i] = Integer.parseInt(tok.nextToken());
		}
	}

	LinuxExport(String p, String h[], String r[], String s[])
	{
	path = p;
	}

	String params()
	{
	String rv = "path="+FileManager.urlize(path)+
		    "&count="+host.length;
	for(int i=0; i<host.length; i++) {
		rv += "&host"+i+"="+FileManager.urlize(host[i]);
		rv += "&ro"+i+"="+(ro[i] ? 1 : 0);
		rv += "&squash"+i+"="+squash[i];
		}
	return rv;
	}
}

class SharingWindow extends FixedFrame implements CbButtonCallback
{
	CbButton save_b, cancel_b;
	RemoteFile file;
	FileManager filemgr;
	SambaShare sshare;
	DFSAdminExport dexport;
	LinuxExport lexport;
	Checkbox samba_on, samba_off;
	Checkbox writable_on, writable_off;
	Checkbox available_on, available_off;
	Checkbox guest_on, guest_off, guest_only;
	TextField comment;

	TextField desc;
	Checkbox nfs_on, nfs_off;
	TextField rwhosts, rohosts, roothosts;
	Checkbox rw[] = new Checkbox[3], ro[] = new Checkbox[3],
		 root[] = new Checkbox[3];

	TextField host[];
	Choice lro[], squash[];

	SharingWindow(RemoteFile f, FileManager p)
	{
	file = f; filemgr = p;
	setTitle(filemgr.text("share_title", file.path));
	sshare = (SambaShare)filemgr.stab.get(file.path);
	Object nshare = filemgr.ntab.get(file.path);
	if (filemgr.nfsmode == 1)
		lexport = (LinuxExport)nshare;
	else if (filemgr.nfsmode == 2)
		dexport = (DFSAdminExport)nshare;

	// setup UI
	setLayout(new BorderLayout());
	Panel samba = new Panel(), sl = new Panel(), sr = new Panel();
	samba.setLayout(new BorderLayout());
	Panel st = new Panel();
	st.setLayout(new GridLayout(2, 1));
	CheckboxGroup sg = new CheckboxGroup();
	st.add(samba_off = new Checkbox(filemgr.text("share_soff"), sg, 
				       sshare == null));
	st.add(samba_on = new Checkbox(filemgr.text("share_son"), sg,
					sshare != null));
	samba.add("North", st);

	Panel stop = new LinedPanel(filemgr.text("share_sheader"));
	setup_leftright(stop, sl, sr);

	comment = new TextField(sshare == null ? "" : sshare.comment, 25);
	comment.setFont(filemgr.fixed);
	add_item(filemgr.text("share_comment"), comment, sl, sr);

	Panel ap = new Panel();
	ap.setLayout(new GridLayout(1, 0));
	CheckboxGroup ag = new CheckboxGroup();
	ap.add(available_on = new Checkbox(filemgr.text("yes"), ag,
					  sshare == null || sshare.available));
	ap.add(available_off = new Checkbox(filemgr.text("no"), ag,
					  sshare != null && !sshare.available));
	add_item(filemgr.text("share_available"), ap, sl, sr);

	Panel wp = new Panel();
	wp.setLayout(new GridLayout(1, 0));
	CheckboxGroup wg = new CheckboxGroup();
	wp.add(writable_on = new Checkbox(filemgr.text("yes"), wg,
					  sshare == null || sshare.writable));
	wp.add(writable_off = new Checkbox(filemgr.text("no"), wg,
					   sshare != null && !sshare.writable));
	add_item(filemgr.text("share_writable"), wp, sl, sr);

	Panel gp = new Panel();
	gp.setLayout(new GridLayout(1, 0));
	CheckboxGroup gg = new CheckboxGroup();
	gp.add(guest_only = new Checkbox(filemgr.text("share_only"), gg,
				sshare != null && sshare.guest == 2));
	gp.add(guest_on = new Checkbox(filemgr.text("yes"), gg,
				sshare == null || sshare.guest == 1));
	gp.add(guest_off = new Checkbox(filemgr.text("no"), gg,
			 	sshare != null && sshare.guest == 0));
	add_item(filemgr.text("share_guest"), gp, sl, sr);

	samba.add("Center", stop);

	// Setup NFS UI
	Panel nfs = new Panel(), nl = new Panel(), nr = new Panel();
	nfs.setLayout(new BorderLayout());
	Panel nt = new Panel();
	nt.setLayout(new GridLayout(2, 1));
	CheckboxGroup ng = new CheckboxGroup();
	nt.add(nfs_off = new Checkbox(filemgr.text("share_noff"), ng, 
				      nshare == null));
	nt.add(nfs_on = new Checkbox(filemgr.text("share_non"), ng,
				     nshare != null));
	nfs.add("North", nt);

	Panel ntop = new LinedPanel(filemgr.text("share_nheader"));
	setup_leftright(ntop, nl, nr);
	if (filemgr.nfsmode == 1) {
		// Linux export mode
		nl.setLayout(new GridLayout(0, 1, 2, 2));
		nr.setLayout(new GridLayout(0, 1, 2, 2));
		nl.add(new Label(filemgr.text("share_host")));
		nr.add(new Label(filemgr.text("share_opts")));
		int c = lexport==null ? 0 : lexport.host.length;
		host = new TextField[c+1];
		lro = new Choice[c+1];
		squash = new Choice[c+1];
		for(int i=0; i<c; i++) {
			host[i] = new TextField(lexport.host[i], 20);
			host[i].setFont(filemgr.fixed);
			lro[i] = robox(lexport.ro[i]);
			squash[i] = squashbox(lexport.squash[i]);
			nl.add(host[i]);
			nr.add(opts_panel(lro[i], squash[i]));
			}
		host[c] = new TextField("", 20);
		host[c].setFont(filemgr.fixed);
		lro[c] = robox(false);
		squash[c] = squashbox(1);
		nl.add(host[c]);
		nr.add(opts_panel(lro[c], squash[c]));
		}
	else if (filemgr.nfsmode == 2) {
		// Solaris share mode
		desc = new TextField(dexport == null ? "" : dexport.desc, 25);
		desc.setFont(filemgr.fixed);
		add_item(filemgr.text("share_desc"), desc, nl, nr);

		rohosts = add_hosts(filemgr.text("share_ro"),
				    dexport == null ? "-" : dexport.ro,
				    ro, nl, nr);
		rwhosts = add_hosts(filemgr.text("share_rw"),
				    dexport == null ? "-" : dexport.rw,
				    rw, nl, nr);
		roothosts = add_hosts(filemgr.text("share_root"),
				    dexport == null ? "-" : dexport.root,
				    root, nl, nr);
		root[1].getParent().remove(root[1]);
		}
	else if (filemgr.nfsmode == 3) {
		}
	nfs.add("Center", ntop);

	// Add the appropriate tabs
	if (filemgr.sambamode && filemgr.nfsmode != 0) {
		TabbedPanel tab = new TabbedPanel();
		tab.addItem(filemgr.text("share_samba"), samba);
		tab.addItem(filemgr.text("share_nfs"), nfs);
		add("Center", tab);
		}
	else if (filemgr.sambamode)
		add("Center", samba);
	else if (filemgr.nfsmode != 0)
		add("Center", nfs);

	// Create save and cancel buttons
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(save_b = new CbButton(filemgr.get_image("save.gif"),
				      filemgr.text("save"),
				      CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == save_b) {
		// Update samba settings on server
		if (sshare != null && samba_on.getState()) {
			// Updating share
			sshare.available = available_on.getState();
			sshare.writable = writable_on.getState();
			sshare.guest = guest_only.getState() ? 2 :
				       guest_on.getState() ? 1 : 0;
			sshare.comment = comment.getText();
			String rv[] = filemgr.get_text(
				"save_share.cgi?"+sshare.params());
			}
		else if (sshare != null) {
			// Deleting share
			String rv[] = filemgr.get_text(
				"save_share.cgi?delete=1&"+sshare.params());
			filemgr.stab.remove(sshare.path);
			}
		else if (samba_on.getState()) {
			// Creating share
			sshare = new SambaShare(file.path,
						available_on.getState(),
						writable_on.getState(),
						guest_only.getState() ? 2 :
						guest_on.getState() ? 1 : 0,
						comment.getText());
			filemgr.stab.put(sshare.path, sshare);
			String rv[] = filemgr.get_text(
				"save_share.cgi?new=1&"+sshare.params());
			}

		// Update NFS settings on server
		if (filemgr.nfsmode == 1) {
			if (lexport != null && nfs_on.getState()) {
				// Updating export
				export_options(lexport);
				String rv[] = filemgr.get_text(
					"save_export.cgi?"+lexport.params());
				}
			else if (lexport != null) {
				// Deleting export
				String rv[] = filemgr.get_text(
				  "save_export.cgi?delete=1&"+lexport.params());
				filemgr.ntab.remove(lexport.path);
				}
			else if (nfs_on.getState()) {
				// Creating export
				lexport = new LinuxExport(file.path, null,
							  null, null);
				export_options(lexport);
				String rv[] = filemgr.get_text(
				  "save_export.cgi?new=1&"+lexport.params());
				filemgr.ntab.put(lexport.path, lexport);
				}
			}
		else if (filemgr.nfsmode == 2) {
			if (dexport != null && nfs_on.getState()) {
				// Updating share
				dexport.desc = desc.getText();
				dexport.ro = ro[0].getState() ? "-" :
					     ro[1].getState() ? "" :
					     rohosts.getText();
				dexport.rw = rw[0].getState() ? "-" :
					     rw[1].getState() ? "" :
					     rwhosts.getText();
				dexport.root = root[0].getState() ? "-" :
					       roothosts.getText();
				String rv[] = filemgr.get_text(
					"save_export.cgi?"+dexport.params());
				}
			else if (dexport != null) {
				// Deleting share
				String rv[] = filemgr.get_text(
				  "save_export.cgi?delete=1&"+dexport.params());
				filemgr.ntab.remove(dexport.path);
				}
			else if (nfs_on.getState()) {
				// Creating new share
				dexport = new DFSAdminExport(file.path,
					desc.getText(),
					ro[0].getState() ? "-" :
					ro[1].getState() ? "" :
					rohosts.getText(),
					rw[0].getState() ? "-" :
					rw[1].getState() ? "" :
					rwhosts.getText(),
					root[0].getState() ? "-" :
					roothosts.getText());
				String rv[] = filemgr.get_text(
				    "save_export.cgi?new=1&"+dexport.params());
				filemgr.ntab.put(dexport.path, dexport);
				}
			}
		else if (filemgr.nfsmode == 3) {
			}

		filemgr.show_files(filemgr.showing_files);
		dispose();
		}
	else if (b == cancel_b)
		dispose();
	}

	void setup_leftright(Panel m, Panel l, Panel r)
	{
	m.setLayout(new BorderLayout());
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", l);
	p.add("Center", r);
	l.setLayout(new GridLayout(0, 1));
	r.setLayout(new GridLayout(0, 1));
	m.add("North", p);
	}

	void add_item(String t, Component c, Panel l, Panel r)
	{
	l.add(new Label(t));
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", c);
	r.add(p);
	}

	TextField add_hosts(String name, String v, Checkbox cb[],
			    Panel l, Panel r)
	{
	Panel p = new Panel();
	p.setLayout(new GridLayout(1, 3));
	CheckboxGroup g = new CheckboxGroup();
	p.add(cb[0] = new Checkbox(filemgr.text("share_none"), g,
				   v.equals("-")));
	p.add(cb[1] = new Checkbox(filemgr.text("share_all"), g,
				   v.length() == 0));
	p.add(cb[2] = new Checkbox(filemgr.text("share_listed"), g,
				   v.length() > 1));
	add_item(name, p, l, r);
	TextField t = new TextField(v.equals("-") ? "" : v, 25);
	t.setFont(filemgr.fixed);
	add_item("", t, l, r);
	return t;
	}

	Choice squashbox(int s)
	{
	Choice rv = new Choice();
	rv.addItem(filemgr.text("share_s0"));
	rv.addItem(filemgr.text("share_s1"));
	rv.addItem(filemgr.text("share_s2"));
	rv.select(s);
	return rv;
	}

	Choice robox(boolean r)
	{
	Choice rv = new Choice();
	rv.addItem(filemgr.text("share_lrw"));
	rv.addItem(filemgr.text("share_lro"));
	rv.select(r ? 1 : 0);
	return rv;
	}

	Panel opts_panel(Component ro, Component squash)
	{
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", ro);
	p.add("East", squash);
	return p;
	}

	void export_options(LinuxExport e)
	{
	int c = 0;
	for(int i=0; i<host.length; i++)
		if (host[i].getText().length() > 0)
			c++;
	e.host = new String[c];
	e.ro = new boolean[c];
	e.squash = new int[c];
	for(int i=0,j=0; i<host.length; i++) {
		if (host[i].getText().trim().length() > 0) {
			e.host[j] = host[i].getText();
			e.ro[j] = lro[i].getSelectedIndex() == 1;
			e.squash[j] = squash[i].getSelectedIndex();
			j++;
			}
		}
	}

}

class SearchWindow extends FixedFrame
	implements CbButtonCallback,MultiColumnCallback
{
	TabbedPanel tab;
	MultiColumn list;
	CbButton search_b, cancel_b, down_b;
	FileManager filemgr;
	TextField dir, match, user, group;
	Checkbox uany, usel, gany, gsel;
	Choice type;
	Checkbox sany, smore, sless;
	TextField more, less;
	Checkbox xon, xoff;
	String types[] = { "", "f", "d", "l", "p" };
	TextField cont;
	RemoteFile results[];

	SearchWindow(String d, FileManager p)
	{
	filemgr = p;
	setTitle(filemgr.text("search_title"));

	// setup UI
	setLayout(new BorderLayout());
	tab = new TabbedPanel();
	Panel search = new Panel();
	search.setLayout(new BorderLayout());
	tab.addItem(filemgr.text("search_crit"), search);
	Panel l = new Panel(), r = new Panel();
	l.setLayout(new GridLayout(0, 1));
	r.setLayout(new GridLayout(0, 1));

	String cols[] = { "", filemgr.text("right_name"),
			  filemgr.text("right_size") };
	float widths[] = { .07f, .78f, .15f };
	list = new MultiColumn(cols, this);
	list.setWidths(widths);
	list.setDrawLines(false);
	list.setFont(filemgr.fixed);
	tab.addItem(filemgr.text("search_list"), list);

	add_item(filemgr.text("search_dir"), dir = new TextField(d, 30), l, r);
	dir.setFont(filemgr.fixed);

	// Filename
	add_item(filemgr.text("search_match"), match = new TextField(20), l, r);
	match.setFont(filemgr.fixed);

	if (filemgr.search_contents) {
		// File contents
		add_item(filemgr.text("search_cont"),
			 cont = new TextField(30), l, r);
		cont.setFont(filemgr.fixed);
		}

	// User or group owners
	if (filemgr.can_users) {
		Panel up = new Panel();
		up.setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
		CheckboxGroup ug = new CheckboxGroup();
		up.add(uany = new Checkbox(filemgr.text("search_any"), ug, true));
		up.add(usel = new Checkbox("", ug, false));
		up.add(user = new TextField(10));
		user.setFont(filemgr.fixed);
		add_item(filemgr.text("search_user"), up, l, r);

		Panel gp = new Panel();
		gp.setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
		CheckboxGroup gg = new CheckboxGroup();
		gp.add(gany = new Checkbox(filemgr.text("search_any"), gg, true));
		gp.add(gsel = new Checkbox("", gg, false));
		gp.add(group = new TextField(10));
		group.setFont(filemgr.fixed);
		add_item(filemgr.text("search_group"), gp, l, r);
		}

	// File type
	if (!filemgr.follow_links) {
		type = new Choice();
		for(int i=0; i<types.length; i++)
			type.addItem(filemgr.text("search_types_"+types[i]));
		add_item(filemgr.text("search_type"), type, l, r);
		}

	// File size
	CheckboxGroup sg = new CheckboxGroup();
	add_item(filemgr.text("search_size"),
		 sany = new Checkbox(filemgr.text("search_any"), sg, true),
		 l, r);
	Panel mp = new Panel();
	mp.setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
	mp.add(smore = new Checkbox(filemgr.text("search_more"), sg, false));
	mp.add(more = new TextField(10));
	more.setFont(filemgr.fixed);
	add_item("", mp, l, r);
	Panel lp = new Panel();
	lp.setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
	lp.add(sless = new Checkbox(filemgr.text("search_less"), sg, false));
	lp.add(less = new TextField(10));
	less.setFont(filemgr.fixed);
	add_item("", lp, l, r);

	if (filemgr.got_filesystems) {
		// Search past mounts
		CheckboxGroup xg = new CheckboxGroup();
		Panel xp = new Panel();
		xp.setLayout(new FlowLayout(FlowLayout.LEFT, 1, 1));
		xp.add(xoff = new Checkbox(filemgr.text("yes"), xg, true));
		xp.add(xon = new Checkbox(filemgr.text("no"), xg, false));
		add_item(filemgr.text("search_xdev"), xp, l, r);
		}

	search.add("West", l); search.add("East", r);
	add("Center", tab);

	// Create search and cancel buttons
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(down_b = new CbButton(filemgr.get_image("down.gif"),
				      filemgr.text("search_down"),
				      CbButton.LEFT, this));
	bot.add(search_b = new CbButton(filemgr.get_image("save.gif"),
				      filemgr.text("search_ok"),
				      CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("cancel"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	pack();
	show();
	}

	void add_item(String t, Component c, Panel l, Panel r)
	{
	l.add(new Label(t));
	Panel p = new Panel();
	p.setLayout(new BorderLayout());
	p.add("West", c);
	r.add(p);
	}

	public void click(CbButton b)
	{
	if (b == cancel_b)
		dispose();
	else if (b == search_b) {
		// validate inputs and build search URL
		String url = "search.cgi";
		String d = dir.getText().trim();
		if (d.length() == 0 || d.charAt(0) != '/') {
			new ErrorWindow(filemgr.text("search_edir"));
			return;
			}
		url += "?dir="+filemgr.urlize(d);
		String mt = match.getText().trim();
		if (mt.length() == 0) {
			mt = "*";
			//new ErrorWindow(filemgr.text("search_ematch"));
			//return;
			}
		url += "&match="+filemgr.urlize(mt);
		if (type != null && type.getSelectedIndex() > 0)
			url += "&type="+types[type.getSelectedIndex()];
		if (usel != null && usel.getState()) {
			String u = user.getText().trim();
			if (u.length() == 0) {
				new ErrorWindow(filemgr.text("search_euser"));
				return;
				}
			url += "&user="+filemgr.urlize(u);
			}
		if (gsel != null && gsel.getState()) {
			String g = group.getText().trim();
			if (g.length() == 0) {
				new ErrorWindow(filemgr.text("search_egroup"));
				return;
				}
			url += "&group="+filemgr.urlize(g);
			}
		if (smore.getState()) {
			String m = more.getText().trim();
			try { Integer.parseInt(m); }
			catch(Exception e) {
				new ErrorWindow(filemgr.text("search_esize"));
				return;
				}
			url += "&size=%2B"+m+"c";
			}
		else if (sless.getState()) {
			String l = less.getText().trim();
			try { Integer.parseInt(l); }
			catch(Exception e) {
				new ErrorWindow(filemgr.text("search_esize"));
				return;
				}
			url += "&size=%2D"+l+"c";
			}
		if (xon != null && xon.getState())
			url += "&xdev=1";
		if (cont != null && cont.getText().trim().length() > 0)
			url += "&cont="+filemgr.urlize(cont.getText());

		// send off the search
		setCursor(WAIT_CURSOR);
		String f[] = filemgr.get_text(url);
		if (f[0].length() > 0) {
			new ErrorWindow(f[0]);
			return;
			}
		Object rows[][] = new Object[f.length-1][];
		results = new RemoteFile[f.length-1];
		for(int i=1; i<f.length; i++) {
			RemoteFile r = new RemoteFile(filemgr, f[i], null);
			results[i-1] = r;
			Object row[] = rows[i-1] = new Object[3];
			row[0] = filemgr.get_image(RemoteFile.tmap[r.type]);
			row[1] = r.path;
			if (r.size < 1000)
				row[2] = filemgr.spad(r.size, 5)+" B";
			else if (r.size < 1000000)
				row[2] = filemgr.spad(r.size/1000, 5)+" kB";
			else
				row[2] = filemgr.spad(r.size/1000000, 5)+" MB";
			}
		list.clear();
		list.addItems(rows);
		tab.select(filemgr.text("search_list"));
		setCursor(DEFAULT_CURSOR);
		}
	else if (b == down_b) {
		// Download selected file (if any)
		int num = list.selected();
		if (num < 0 || results.length == 0) {
			new ErrorWindow(filemgr.text("search_edown"));
			return;
			}
		filemgr.download_file(results[num]);
		}
	}

	public void singleClick(MultiColumn list, int num)
	{
	}

	// go to the directory of the double-clicked file
	public void doubleClick(MultiColumn list, int num)
	{
	RemoteFile f = results[num];
	int sl = f.path.lastIndexOf('/');
	String dir = sl == 0 ? "/" : f.path.substring(0, sl);
	filemgr.find_directory(dir, true);
	RemoteFile l[] = filemgr.showing_list;
	for(int i=0; i<l.length; i++) {
		if (l[i].name.equals(f.name)) {
			// select the file in the list
			filemgr.files.select(i+1);
			filemgr.files.scrollto(i+1);
			break;
			}
		}
	dispose();
	}

	public void headingClicked(MultiColumn list, int col)
	{
	}
}

// A popup window showing previously entered paths
class HistoryWindow extends FixedFrame
	implements CbButtonCallback,ActionListener
{

	java.awt.List hlist;
	CbButton ok_b, cancel_b;
	FileManager filemgr;

	HistoryWindow(FileManager p)
	{
	filemgr = p;
	setTitle(filemgr.text("history_title"));

	// Setup UI
	hlist = new java.awt.List();
	for(int i=0; i<filemgr.history_list.size(); i++) {
		hlist.add((String)filemgr.history_list.elementAt(i));
		}
	hlist.addActionListener(this);
	setLayout(new BorderLayout());
	add("Center", hlist);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(ok_b = new CbButton(filemgr.get_image("save.gif"),
				    filemgr.text("history_ok"),
				    CbButton.LEFT, this));
	bot.add(cancel_b = new CbButton(filemgr.get_image("cancel.gif"),
                                        filemgr.text("cancel"),
                                        CbButton.LEFT, this));
	add("South", bot);
        Util.recursiveBody(this);
        pack();
        show();
	}

	public void click(CbButton b)
	{
	if (b == cancel_b)
		dispose();
	else if (b == ok_b) {
		// Go to the selected directory
		String p = hlist.getSelectedItem();
		if (p != null) {
			filemgr.find_directory(p, true);
			dispose();
			}
		}
	}

	public void actionPerformed(ActionEvent e)
	{
	// List entry double-clicked .. go to it
	String p = hlist.getSelectedItem();
	filemgr.find_directory(p, true);
	dispose();
	}

	public Dimension minimumSize()
	{
	return new Dimension(300, 300);
	}
}

class FileSystem
{
	String mount;
	String dev;
	String type;
	String opts[];
	boolean acls;
	boolean attrs;
	boolean ext;
	boolean mtab, fstab;

	FileSystem(String l)
	{
	StringSplitter tok = new StringSplitter(l, ' ');
	mount = tok.nextToken();
	dev = tok.nextToken();
	type = tok.nextToken();
	String optstr = tok.nextToken();
	acls = tok.nextToken().equals("1");
	attrs = tok.nextToken().equals("1");
	ext = tok.nextToken().equals("1");
	mtab = tok.nextToken().equals("1");
	fstab = tok.nextToken().equals("1");

	StringTokenizer tok2 = new StringTokenizer(optstr, ",");
	opts = new String[tok2.countTokens()];
	for(int i=0; i<opts.length; i++)
		opts[i] = tok2.nextToken();
	}
}

class ACLEntry
{
	FileManager filemgr;
	RemoteFile file;
	boolean def;
	String type;
	String owner;
	boolean read, write, exec;
	boolean empty_owner = false;

	ACLEntry(String l, ACLWindow w)
	{
	filemgr = w.filemgr;
	file = w.file;
	StringSplitter tok = new StringSplitter(l, ':');
	type = tok.nextToken();
	if (type.equals("default")) {
		def = true;
		type = tok.nextToken();
		}
	if (!type.equals("mask") && !type.equals("other")) {
		owner = tok.nextToken();
		if (owner.length() == 0)
			owner = null;
		}
	String rwx = tok.nextToken();
	if (rwx.length() == 0) {
		rwx = tok.nextToken();	// getfacl outputs a blank owner for
					// mask and other on some systems
		empty_owner = true;
		}
	read = (rwx.charAt(0) == 'r');
	write = (rwx.charAt(1) == 'w');
	exec = (rwx.charAt(2) == 'x');
	}

	ACLEntry(ACLWindow w)
	{
	filemgr = w.filemgr;
	file = w.file;
	}

	String[] getRow()
	{
	String rv[] = new String[3];
	String t = def ? "acltype_default_"+type : "acltype_"+type;
	rv[0] = filemgr.text(t);
	if (type.equals("mask") || type.equals("other") ||
	    (def && owner == null))
		rv[1] = "";
	else if (owner != null)
		rv[1] = owner;
	else if (type.equals("user"))
		rv[1] = filemgr.text("eacl_user", file.user);
	else
		rv[1] = filemgr.text("eacl_group", file.group);
	rv[2] = "";
	if (read) rv[2] += filemgr.text("info_read")+" ";
	if (write) rv[2] += filemgr.text("info_write")+" ";
	if (exec) rv[2] += filemgr.text("info_exec")+" ";
	return rv;
	}

	public String toString()
	{
	String rv = def ? "default:" : "";
	rv += type+":";
	if (!type.equals("mask") && !type.equals("other") || empty_owner)
		// mask and other types have no owner field at all, except
		// on some operating systems like FreeBSD where it is empty
		rv += (owner == null ? "" : owner)+":";
	rv += (read ? 'r' : '-');
	rv += (write ? 'w' : '-');
	rv += (exec ? 'x' : '-');
	return rv;
	}
}

class ACLEditor extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	ACLWindow aclwin;
	ACLEntry acl;
	boolean creating;
	CbButton ok, del;
	Checkbox read, write, exec, owner1, owner2;
	TextField owner;

	// Editing an existing ACL entry
	ACLEditor(ACLWindow w, ACLEntry a)
	{
	aclwin = w;
	filemgr = aclwin.filemgr;
	acl = a;
	creating = false;
	makeUI();
	}

	// Creating a new ACL entry
	ACLEditor(ACLWindow w, String type, boolean def, boolean empty_owner)
	{
	aclwin = w;
	filemgr = aclwin.filemgr;
	acl = new ACLEntry(aclwin);
	acl.def = def;
	acl.type = type;
	acl.empty_owner = empty_owner;
	creating = true;
	makeUI();
	}

	void makeUI()
	{
	setTitle(filemgr.text(creating ? "eacl_create" : "eacl_edit"));
	setLayout(new BorderLayout());
	Panel left = new Panel();
	left.setLayout(new GridLayout(0, 1));
	add("West", left);
	Panel right = new Panel();
	right.setLayout(new GridLayout(0, 1));
	add("East", right);

	left.add(new Label(filemgr.text("eacl_acltype")));
	TextField type;
	right.add(type = new TextField(
				(acl.def ? "default " : "")+acl.type, 20));
	type.setEditable(false);
	type.setFont(filemgr.fixed);

	if (!acl.type.equals("mask") && !acl.type.equals("other")) {
		left.add(new Label(filemgr.text("eacl_aclname")));
		if (acl.def) {
			// A default user or group ACL .. can be for
			// a specific user, or for the file owner
			Panel op = new Panel();
			op.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
			CheckboxGroup gr = new CheckboxGroup();
			op.add(owner1 = new Checkbox(filemgr.text("eacl_owner"),
					    gr, acl.owner == null));
			op.add(owner2 = new Checkbox("",
					    gr, acl.owner != null));
			op.add(owner = new TextField(
				acl.owner == null ? "" : acl.owner, 20));
			owner.setFont(filemgr.fixed);
			right.add(op);
			}
		else if (creating || acl.owner != null) {
			// A user or group ACL for a specific user
			owner = new TextField(
					acl.owner == null ? "" : acl.owner, 20);
			owner.setFont(filemgr.fixed);
			right.add(owner);
			}
		else {
			// A user or group ACL for the file owner
			String str;
			if (acl.type.equals("user"))
			    str = filemgr.text("eacl_user", aclwin.file.user);
			else
			    str = filemgr.text("eacl_group", aclwin.file.group);
			TextField o = new TextField(str);
			o.setEditable(false);
			o.setFont(filemgr.fixed);
			right.add(o);
			}
		}

	left.add(new Label(filemgr.text("eacl_aclperms")));
	Panel pp = new Panel();
	pp.setLayout(new FlowLayout(FlowLayout.RIGHT));
	pp.add(read = new Checkbox(filemgr.text("info_read"), null, acl.read));
	pp.add(write = new Checkbox(filemgr.text("info_write"), null, acl.write));
	pp.add(exec = new Checkbox(filemgr.text("info_exec"), null, acl.exec));
	right.add(pp);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				  filemgr.text("save"),
				  CbButton.LEFT, this));
	if (!creating && (acl.owner != null || acl.def))
		bot.add(del = new CbButton(filemgr.get_image("cancel.gif"),
					   filemgr.text("delete"),
					   CbButton.LEFT, this));
	add("South", bot);

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == ok) {
		// Update or add the ACL entry
		if (owner1 != null && owner1.getState()) {
			acl.owner = null;
			}
		else if (owner != null) {
			String o = owner.getText().trim();
			if (o.length() == 0 && !acl.def) {
				new ErrorWindow(filemgr.text("eacl_eowner"));
				return;
				}
			acl.owner = owner.getText();
			if (acl.owner.length() == 0)
				acl.owner = null;
			}
		acl.read = read.getState();
		acl.write = write.getState();
		acl.exec = exec.getState();
		if (creating) {
			// Add to the ACL table
			aclwin.acllist.addElement(acl);
			aclwin.acltable.addItem(acl.getRow());
			}
		else {
			// Update the table
			int idx = aclwin.acllist.indexOf(acl);
			aclwin.acltable.modifyItem(acl.getRow(), idx);
			}
		dispose();
		}
	else if (b == del) {
		// Remove this entry
		int idx = aclwin.acllist.indexOf(acl);
		aclwin.acllist.removeElementAt(idx);
		aclwin.acltable.deleteItem(idx);
		dispose();
		}
	}

	public void dispose()
	{
	aclwin.edmap.remove(acl);
	super.dispose();
	}
}

class ACLWindow extends FixedFrame implements CbButtonCallback,MultiColumnCallback
{
	FileManager filemgr;
	RemoteFile file;
	Vector acllist = new Vector();
	Hashtable edmap = new Hashtable();

	CbButton ok, cancel, add;
	Choice addtype;
	MultiColumn acltable;

	String acltypes[] = { "user", "group", "mask",
			      "default user", "default group", "default other",
			      "default mask" };

	ACLWindow(FileManager p, RemoteFile f)
	{
	super(400, 300);
	setTitle(p.text("eacl_title", f.path));
	filemgr = p;
	file = f;

	// Get the ACLs
	String a[] = filemgr.get_text(
			"getfacl.cgi?file="+filemgr.urlize(file.path));
	if (a[0].length() != 0) {
		new ErrorWindow(filemgr.text("eacl_eacls", a[0]));
		return;
		}

	// Create the UI
	setLayout(new BorderLayout());
	String titles[] = { filemgr.text("eacl_acltype"),
			    filemgr.text("eacl_aclname"),
			    filemgr.text("eacl_aclperms") };
	acltable = new MultiColumn(titles, this);
	for(int i=1; i<a.length; i++) {
		ACLEntry acl = new ACLEntry(a[i], this);
		acllist.addElement(acl);
		acltable.addItem(acl.getRow());
		}
	add("Center", acltable);
	Panel abot = new Panel();
	abot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	abot.add(add = new CbButton(filemgr.get_image("add.gif"),
				   filemgr.text("eacl_add"),
				   CbButton.LEFT, this));
	int len = file.type == RemoteFile.DIR ? acltypes.length : 3;
	abot.add(addtype = new Choice());
	for(int i=0; i<len; i++) {
		String t = "acltype_"+acltypes[i].replace(' ', '_');
		addtype.addItem(filemgr.text(t));
		}
	abot.add(new Label(" "));
	abot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				   filemgr.text("save"),
				   CbButton.LEFT, this));
	abot.add(cancel = new CbButton(filemgr.get_image("cancel.gif"),
				       filemgr.text("cancel"),
				       CbButton.LEFT, this));
	add("South", abot);

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == ok) {
		// Check if there are any defaults, and if so there must
		// be default user, group and other
		boolean anydef = false, defuser = false,
			defgroup = false, defother = false;
		for(int i=0; i<acllist.size(); i++) {
			ACLEntry e = (ACLEntry)acllist.elementAt(i);
			if (e.def) anydef = true;
			if (e.def && e.owner == null) {
				if (e.type.equals("user")) defuser = true;
				if (e.type.equals("group")) defgroup = true;
				if (e.type.equals("other")) defother = true;
				}
			}
		if (anydef && (!defuser || !defgroup || !defother)) {
			new ErrorWindow(filemgr.text("eacl_edefaults"));
			return;
			}

		// Save the ACLs
		String aclstr = "";
		for(int i=0; i<acllist.size(); i++)
			aclstr += (ACLEntry)acllist.elementAt(i)+"\n";
		String rv[] = filemgr.get_text("setfacl.cgi?file="+
						filemgr.urlize(file.path)+
						"&acl="+filemgr.urlize(aclstr));
		if (rv[0].length() > 0)
			new ErrorWindow(filemgr.text("eacl_efailed",
				file.path, rv[0]));
		else
			dispose();
		}
	else if (b == add) {
		// Open a window for a new ACL entry
		String t = acltypes[addtype.getSelectedIndex()];
		String d = "default ";
		boolean def = t.startsWith(d);
		if (def)
			t = t.substring(d.length());
		if (t.equals("mask")) {
			// Only allow one mask
			for(int i=0; i<acllist.size(); i++) {
				ACLEntry a = (ACLEntry)acllist.elementAt(i);
				if (a.type.equals(t) && a.def == def) {
					new ErrorWindow(filemgr.text(def ?
					    "eacl_edefmask" : "eacl_emask"));
					return;
					}
				}
			}
		// Check if owner field exists and is empty for existing
		// mask or other fields
		boolean new_empty_owner = false;
		for(int i=0; i<acllist.size(); i++) {
			ACLEntry a = (ACLEntry)acllist.elementAt(i);
			if ((a.type.equals("mask") || a.type.equals("other")) &&
			   a.empty_owner) {
				new_empty_owner = true;
				}
			}
		new ACLEditor(this, t, def, new_empty_owner);
		}
	else if (b == cancel) {
		// Don't save
		dispose();
		}
	}

	// Bring up an editor for an ACL
        public void doubleClick(MultiColumn list, int num)
	{
	int idx = list.selected();
	if (idx >= 0) {
		ACLEntry e = (ACLEntry)acllist.elementAt(idx);
		ACLEditor ed = (ACLEditor)edmap.get(e);
		if (ed == null)
			edmap.put(e, new ACLEditor(this, e));
		else {
			ed.toFront();
			ed.requestFocus();
			}
		}
	}

        public void singleClick(MultiColumn list, int num)
	{
	}

	public void headingClicked(MultiColumn list, int col)
	{
	}
}

class AttributesWindow extends FixedFrame
	implements CbButtonCallback,MultiColumnCallback
{
	FileManager filemgr;
	RemoteFile file;
	Vector attrlist = new Vector();
	Hashtable edmap = new Hashtable();

	CbButton ok, cancel, add;
	MultiColumn attrtable;

	AttributesWindow(FileManager p, RemoteFile f)
	{
	super(400, 300);
	setTitle(p.text("attr_title", f.path));
	filemgr = p;
	file = f;

	// Get the attributes
	String a[] = filemgr.get_text(
			"getattrs.cgi?file="+filemgr.urlize(file.path));
	if (a[0].length() != 0) {
		new ErrorWindow(filemgr.text("attr_eattrs", a[0]));
		return;
		}

	// Create the UI
	setLayout(new BorderLayout());
	String titles[] = { filemgr.text("attr_name"),
			    filemgr.text("attr_value") };
	attrtable = new MultiColumn(titles, this);
	for(int i=1; i<a.length; i++) {
		FileAttribute at = new FileAttribute(a[i], filemgr);
		attrlist.addElement(at);
		attrtable.addItem(at.getRow());
		}
	add("Center", attrtable);
	Panel abot = new Panel();
	abot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	abot.add(add = new CbButton(filemgr.get_image("add.gif"),
				   filemgr.text("attr_add"),
				   CbButton.LEFT, this));
	abot.add(new Label(" "));
	abot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				   filemgr.text("save"),
				   CbButton.LEFT, this));
	abot.add(cancel = new CbButton(filemgr.get_image("cancel.gif"),
				       filemgr.text("cancel"),
				       CbButton.LEFT, this));
	add("South", abot);

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == ok) {
		// Save the attributes
		String pstr = "";
		for(int i=0; i<attrlist.size(); i++) {
			FileAttribute at = (FileAttribute)attrlist.elementAt(i);
			pstr += "&name"+i+"="+filemgr.urlize(at.name)+
			        "&value"+i+"="+filemgr.urlize(at.value);
			}
		String rv[] = filemgr.get_text("setattrs.cgi?file="+
						filemgr.urlize(file.path)+pstr);
		if (rv[0].length() > 0)
			new ErrorWindow(filemgr.text("attr_efailed",
				file.path, rv[0]));
		else
			dispose();
		}
	else if (b == add) {
		// Open a window for a new ACL entry
		new AttributeEditor(this);
		}
	else if (b == cancel) {
		// Don't save
		dispose();
		}
	}

	// Bring up an editor for an ACL
        public void doubleClick(MultiColumn list, int num)
	{
	int idx = list.selected();
	if (idx >= 0) {
		FileAttribute at = (FileAttribute)attrlist.elementAt(idx);
		AttributeEditor ed = (AttributeEditor)edmap.get(at);
		if (ed == null)
			edmap.put(at, new AttributeEditor(this, at));
		else {
			ed.toFront();
			ed.requestFocus();
			}
		}
	}

        public void singleClick(MultiColumn list, int num)
	{
	}

	public void headingClicked(MultiColumn list, int col)
	{
	}
}

class FileAttribute
{
	String name;
	String value;

	FileAttribute(String l, FileManager f)
	{
	int eq = l.indexOf('=');
	name = f.un_urlize(l.substring(0, eq));
	value = f.un_urlize(l.substring(eq+1));
	}

	FileAttribute(String n, String v)
	{
	name = n;
	value = v;
	}

	String[] getRow()
	{
	return new String[] { name, value };
	}
}

class AttributeEditor extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	AttributesWindow attrwin;
	FileAttribute attr;
	boolean creating;
	CbButton ok, del;
	TextField name;
	TextArea value;

	AttributeEditor(AttributesWindow w, FileAttribute a)
	{
	attrwin = w;
	attr = a;
	filemgr = w.filemgr;
	creating = false;
	makeUI();
	}

	AttributeEditor(AttributesWindow w)
	{
	attrwin = w;
	attr = new FileAttribute("", "");
	filemgr = w.filemgr;
	creating = true;
	makeUI();
	}

	void makeUI()
	{
	setTitle(filemgr.text(creating ? "attr_create" : "attr_edit"));
	setLayout(new BorderLayout());

	Panel top = new Panel();
	top.setLayout(new GridLayout(1, 2));
	top.add(new Label(filemgr.text("attr_name")));
	top.add(name = new TextField(attr.name, 20));
	name.setFont(filemgr.fixed);
	add("North", top);

	Panel mid = new Panel();
	mid.setLayout(new GridLayout(1, 2));
	mid.add(new Label(filemgr.text("attr_value")));
	mid.add(value = new TextArea(attr.value, 5, 20));
	add("Center", mid);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				  filemgr.text("save"),
				  CbButton.LEFT, this));
	if (!creating)
		bot.add(del = new CbButton(filemgr.get_image("cancel.gif"),
					   filemgr.text("delete"),
					   CbButton.LEFT, this));
	add("South", bot);

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == ok) {
		// Update or add the attribute
		if (name.getText().length() == 0) {
			new ErrorWindow(filemgr.text("attr_ename"));
			return;
			}
		attr.name = name.getText();
		attr.value = value.getText();
		if (creating) {
			// Add to the attribs table
			attrwin.attrlist.addElement(attr);
			attrwin.attrtable.addItem(attr.getRow());
			}
		else {
			// Update the table
			int idx = attrwin.attrlist.indexOf(attr);
			attrwin.attrtable.modifyItem(attr.getRow(), idx);
			}
		dispose();
		}
	else if (b == del) {
		// Remove this entry
		int idx = attrwin.attrlist.indexOf(attr);
		attrwin.attrlist.removeElementAt(idx);
		attrwin.attrtable.deleteItem(idx);
		dispose();
		}
	}

	public void dispose()
	{
	attrwin.edmap.remove(attr);
	super.dispose();
	}
}

class EXTWindow extends FixedFrame implements CbButtonCallback
{
	FileManager filemgr;
	RemoteFile file;

	CbButton ok, cancel;
	Checkbox cbs[];

	String attrs[] = { "A", "a", "c", "d", "i", "s", "S", "u" };
	Hashtable attrmap = new Hashtable();

	EXTWindow(FileManager p, RemoteFile f)
	{
	super();
	setTitle(p.text("ext_title", f.path));
	filemgr = p;
	file = f;

	// Get the attributes
	String a[] = filemgr.get_text(
			"getext.cgi?file="+filemgr.urlize(file.path));
	if (a[0].length() != 0) {
		new ErrorWindow(filemgr.text("ext_eattrs", a[0]));
		return;
		}
	for(int i=0; i<a[1].length(); i++)
		attrmap.put(a[1].substring(i, i+1), "");

	// Create the UI
	setLayout(new BorderLayout());
	Panel top = new LinedPanel(filemgr.text("ext_header"));
	top.setLayout(new GridLayout(0, 1));
	cbs = new Checkbox[attrs.length];
	for(int i=0; i<attrs.length; i++) {
		cbs[i] = new Checkbox(filemgr.text("eattr_"+attrs[i]));
		cbs[i].setState(attrmap.get(attrs[i]) != null);
		top.add(cbs[i]);
		}
	add("Center", top);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(ok = new CbButton(filemgr.get_image("save.gif"),
				  filemgr.text("save"),
				  CbButton.LEFT, this));
	bot.add(cancel = new CbButton(filemgr.get_image("cancel.gif"),
				      filemgr.text("cancel"),
				      CbButton.LEFT, this));
	add("South", bot);

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == ok) {
		// Save the attributes (including unknown ones)
		String astr = "";
		for(int i=0; i<cbs.length; i++) {
			if (cbs[i].getState())
				astr += attrs[i];
			attrmap.remove(attrs[i]);
			}
		for(Enumeration e = attrmap.keys(); e.hasMoreElements(); )
			astr += e.nextElement();

		// Try to set on the server
		String rv[] = filemgr.get_text("setext.cgi?file="+
				filemgr.urlize(file.path)+"&attrs="+astr);
		if (rv[0].length() > 0)
			new ErrorWindow(filemgr.text("ext_efailed",
				file.path, rv[0]));
		else
			dispose();
		}
	else if (b == cancel) {
		dispose();
		}
	}
}

class MountWindow extends FixedFrame implements CbButtonCallback
{
	CbButton yes, no;
	FileManager filemgr;
	FileSystem fs;
	RemoteFile file;

	MountWindow(FileManager filemgr, FileSystem fs, RemoteFile file)
	{
	super();
	setTitle(filemgr.text(fs.mtab ? "mount_title2" : "mount_title1"));
	this.filemgr = filemgr;
	this.fs = fs;
	this.file = file;

	// Create the UI
	setLayout(new BorderLayout());
	Panel cen = new BorderPanel(1, Util.body);
	cen.setLayout(new GridLayout(1, 1));
	String rusure = fs.mtab ? "mount_rusure2" : "mount_rusure1";
	cen.add(new Label(filemgr.text(rusure, fs.mount, fs.dev)));
	add("Center", cen);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(yes = new CbButton(filemgr.text("yes"), this));
	bot.add(no = new CbButton(filemgr.text("no"), this));
	add("South", bot);
	pack();
	show();
	Util.recursiveBody(this);
	}

	public void click(CbButton b)
	{
	if (b == yes) {
		// Go ahread and do it!
		String rv[] = filemgr.get_text("mount.cgi?dir="+
					       filemgr.urlize(fs.mount));
		dispose();
		if (rv[0].equals("")) {
			// It worked - refresh this directory and the mount list
			filemgr.get_filesystems();
			FileNode d = (FileNode)filemgr.nodemap.get(file);
			if (d != null) {
				d.setimage();
				d.known = false;
				d.file.list = null;
				d.fill();
				}
			if (fs.mtab)
				filemgr.show_files(file.directory);
			else
				filemgr.show_files(filemgr.showing_files);
			}
		else {
			// Failed - show the error
			new ErrorWindow(filemgr.text(
				fs.mtab ? "mount_err2" : "mount_err1",
				fs.mount, rv[0]));
			}
		}
	else {
		// Just close the window
		dispose();
		}
	}
}

// A label that is limited to a maximum number of characters wide
class MultiLabel extends BorderPanel
{
	public MultiLabel(String s, int max)
	{
	this(s, max, 1);
	}

	public MultiLabel(String s, int max, int b)
	{
	this(s, max, b, Label.CENTER);
	}

	
	public MultiLabel(String s, int max, int b, int align)
	{
	super(b, Util.body);
	Vector v = new Vector();
	StringTokenizer tok = new StringTokenizer(s.trim(), " \t");
	String line = null;
	while(tok.hasMoreTokens()) {
		String w = tok.nextToken();
		line = (line == null ? w : line+" "+w);
		if (line.length() > max || !tok.hasMoreTokens()) {
			v.addElement(line);
			line = null;
			}
		}
	setLayout(new GridLayout(v.size(), 1, 0, 0));
	for(int i=0; i<v.size(); i++) {
		Label l = new Label((String)v.elementAt(i), Label.CENTER);
		add(l);
		}
	}
}

// A window for choosing the format in which a directory will be downloaded
class DownloadDirWindow extends FixedFrame implements CbButtonCallback
{
	CbButton zip, tgz, tar, cancel;
	FileManager filemgr;
	RemoteFile file;

	DownloadDirWindow(FileManager filemgr, RemoteFile file)
	{
	super();
	setTitle(filemgr.text("ddir_title"));
	this.filemgr = filemgr;
	this.file = file;

	// Create the UI
	setLayout(new BorderLayout());
	Panel cen = new BorderPanel(1, Util.body);
	cen.setLayout(new GridLayout(1, 1));
	cen.add(new Label(filemgr.text("ddir_rusure", file.path)));
	add("Center", cen);

	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(zip = new CbButton(filemgr.text("ddir_zip"), this));
	bot.add(tgz = new CbButton(filemgr.text("ddir_tgz"), this));
	bot.add(tar = new CbButton(filemgr.text("ddir_tar"), this));
	bot.add(cancel = new CbButton(filemgr.text("cancel"), this));
	add("South", bot);
	pack();
	show();
	Util.recursiveBody(this);
	}

	public void click(CbButton b)
	{
	if (b == cancel) {
		// just close the window
		dispose();
		}
	else {
		// open the download window
		int format = b == zip ? 1 :
			     b == tgz ? 2 : 3;
		dispose();
		filemgr.open_file_window(file, true, format);
		}
	}
}

class PreviewWindow extends Frame implements CbButtonCallback
{
	CbButton close_b;
	RemoteFile file;
	FileManager filemgr;
	ImagePanel ip;

	// Previewing a file
	public PreviewWindow(FileManager p, RemoteFile f)
	{
	//super(350, 350);
	file = f; filemgr = p;
	makeUI();
	setTitle(filemgr.text("preview_title", file.path));

	// Load the file
	try {
		URL u = new URL(filemgr.getDocumentBase(),
				"preview.cgi"+filemgr.urlize(file.path)+
				"?rand="+System.currentTimeMillis()+
				"&trust="+filemgr.trust+
				filemgr.extra);
		URLConnection uc = u.openConnection();
		filemgr.set_cookie(uc);
		int len = uc.getContentLength();
		InputStream is = uc.getInputStream();
		byte buf[];
		if (len >= 0) {
			// Length is known
			buf = new byte[uc.getContentLength()];
			int got = 0;
			while(got < buf.length)
				got += is.read(buf, got, buf.length-got);
			}
		else {
			// Length is unknown .. read till the end
			buf = new byte[0];
			while(true) {
			    byte data[] = new byte[16384];
			    int got;
			    try { got = is.read(data); }
			    catch(EOFException ex) { break; }
			    if (got <= 0) break;
			    byte nbuf[] = new byte[buf.length + got];
			    System.arraycopy(buf, 0, nbuf, 0, buf.length);
			    System.arraycopy(data, 0, nbuf, buf.length, got);
			    buf = nbuf;
			    }
			}

		// Check if this is really an error
		if (uc.getContentType().equals("text/plain")) {
			String s = new String(buf, 0);
			new ErrorWindow(s);
			dispose();
			return;
			}

		// Show the image
		Image img = Toolkit.getDefaultToolkit().createImage(buf);
                MediaTracker waiter = new MediaTracker(this);
                waiter.addImage(img, 666);
                try { waiter.waitForAll(); }
                catch(InterruptedException e) { }
		if (img.getWidth(this) <= 0) {
			new ErrorWindow(filemgr.text("preview_bad"));
			dispose();
			return;
			}
		ip.setImage(img);

		pack();
		show();
		}
	catch(Exception e) { e.printStackTrace(); }
	}

	void makeUI()
	{
	setLayout(new BorderLayout());

	// Image viewing area
	BorderPanel mid = new BorderPanel(2, Util.body);
	mid.setLayout(new BorderLayout());
	ip = new ImagePanel(null);
	mid.add("Center", ip);
	add("Center", mid);

	// Button panel
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(close_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("close"),
					CbButton.LEFT, this));
	add("South", bot);
	Util.recursiveBody(this);
	}

	public void click(CbButton b)
	{
	if (b == close_b) {
		// Just close
		dispose();
		}
	}
}

class ImagePanel extends Panel
{
	Image img;

	public ImagePanel(Image img)
	{
	this.img = img;
	}

	public void paint(Graphics g)
	{
	if (img != null) {
		g.drawImage(img, 0, 0, this);
		}
	}

	public void setImage(Image img)
	{
	this.img = img;
	repaint();
	}

	public Dimension minimumSize()
	{
	return new Dimension(img.getWidth(this), img.getHeight(this));
	}

	public Dimension preferredSize()
	{
	return minimumSize();
	}
}

class ExtractWindow extends FixedFrame implements CbButtonCallback
{
	CbButton yes, yesdelete, no, show;
	FileManager filemgr;
	RemoteFile file;

	ExtractWindow(FileManager filemgr, RemoteFile file)
	{
	super();
	setTitle(filemgr.text("extract_title"));
	this.filemgr = filemgr;
	this.file = file;

	// Create the UI
	setLayout(new BorderLayout());
	Panel cen = new BorderPanel(1, Util.body);
	cen.setLayout(new GridLayout(3, 1));
	cen.add(new Label(filemgr.text("extract_rusure")));
	cen.add(new Label(file.path));
	cen.add(new Label(filemgr.text("extract_rusure2")));
	add("Center", cen);
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.CENTER));
	bot.add(yes = new CbButton(filemgr.text("yes"), this));
	bot.add(yesdelete = new CbButton(filemgr.text("extract_yes"), this));
	bot.add(no = new CbButton(filemgr.text("no"), this));
	bot.add(show = new CbButton(filemgr.text("extract_show"), this));
	add("South", bot);
	pack();
	show();
	Util.recursiveBody(this);
	}

	public void click(CbButton b)
	{
	if (b == yes || b == yesdelete) {
		// Go ahread and do it!
		String rv[] = filemgr.get_text("extract.cgi?file="+
				       filemgr.urlize(file.path)+
				       "&delete="+(b == yesdelete ? 1 : 0));
		dispose();
		if (rv[0].equals("")) {
			// It worked - refresh the directory
			RemoteFile par = file.directory;
			FileNode d = (FileNode)filemgr.nodemap.get(par);
			if (d != null) {
				d.setimage();
				d.known = false;
				d.file.list = null;
				d.fill();
				}
			filemgr.show_files(filemgr.showing_files);
			}
		else {
			// Failed - show the error
			new ErrorWindow(filemgr.text("extract_err", rv[0]));
			}
		}
	else if (b == show) {
		// Open window just showing contents
		String rv[] = filemgr.get_text("contents.cgi?file="+
				       	       filemgr.urlize(file.path));
		dispose();
		if (rv[0].equals("")) {
			// Worked - show the files
			new ContentsWindow(file, filemgr, rv);
			}
		else {
			// Failed - show the error
			new ErrorWindow(filemgr.text("extract_err2", rv[0]));
			}
		}
	else {
		// Just close the window
		dispose();
		}
	}
}

class ContentsWindow extends FixedFrame implements CbButtonCallback
{
	RemoteFile file;
        FileManager filemgr;
        CbButton close_b;

	ContentsWindow(RemoteFile f, FileManager p, String rv[])
	{
	file = f;
	filemgr = p;

	// Create UI
	setTitle(f.path);
	setLayout(new BorderLayout());
	Panel bot = new Panel();
	bot.setLayout(new FlowLayout(FlowLayout.RIGHT));
	bot.add(close_b = new CbButton(filemgr.get_image("cancel.gif"),
					filemgr.text("close"),
					CbButton.LEFT, this));
	add("South", bot);

	// Create text area showing contents
	String lines = "";
	for(int i=1; i<rv.length; i++) {
		lines = lines + rv[i] + "\n";
		}
	TextArea contents = new TextArea(lines, 30, 60);
	contents.setEditable(false);
	add("Center", contents);
	add("North", new Label(filemgr.text("extract_shown")));

	Util.recursiveBody(this);
	pack();
	show();
	}

	public void click(CbButton b)
	{
	if (b == close_b) {
		// Just close
		dispose();
		}
	}
}

