Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / perl / CXAssocXDG.pm

# (c) Copyright 2005-2014. CodeWeavers, Inc.
package CXAssocXDG;
use warnings;
use strict;

use CXLog;
use CXUtils;
use CXAssoc;
use base "CXAssoc";
use CXTinySAX;


#####
#
# MIME database helper functions
#
#####

{
    package Xml2MimeDB;
    use CXTinySAXBase;
    use base "CXTinySAXBase";
    use CXLog;
    use CXUtils;

    sub new($$)
    {
        my ($class, $xdg)=@_;
        my $self={ xdg => $xdg };
        bless $self, $class;
        return undef if (!$self->init());
        return $self;
    }

    sub encoding($$)
    {
        my ($self, $encoding)=@_;
        if ($encoding =~ /^utf-?8$/i)
        {
            delete $self->{encoding};
        }
        else
        {
            cxlog("re-encode from '$encoding' to 'utf8'\n");
            $self->{encoding}=$encoding;
        }
        return 1;
    }

    sub recode($$)
    {
        my ($self, $str)=@_;
        if (defined $self->{encoding})
        {
            require CXRecode;
            $str=CXRecode::from_to($str, $self->{encoding}, "utf8");
        }
        return $str;
    }

    sub start_element($$$)
    {
        my ($self, $element, $attributes)=@_;
        if ($element eq "mime-type" and $attributes->{type})
        {
            $self->{mimes}=[$self->recode($attributes->{type})];
        }
        elsif ($element eq "glob" and $attributes->{pattern})
        {
            my $extension=$self->recode($attributes->{pattern});
            $extension =~ s/^\*\.//;
            $extension =~ tr/A-Z/a-z/; # extensions are case-insensitive in XDG
            push @{$self->{extensions}}, $extension;

        }
        elsif ($element eq "alias" and $attributes->{type})
        {
            push @{$self->{mimes}}, $self->recode($attributes->{type});
        }

        return 1;
    }

    sub end_element($$)
    {
        my ($self, $element)=@_;
        if ($element eq "mime-type" and @{$self->{mimes}})
        {
            foreach my $mimetype (@{$self->{mimes}})
            {
                # XDG merges all the files together so take it all
                $self->{xdg}->mdb_add_mime($mimetype, $self->{extensions});
            }
            $self->{mimes}=[];
            $self->{extensions}=[];
        }
        return 1;
    }
}

# In this function we want to get a list of the *native XDG* MIME types
# and their associated extensions. We don't want to include any MIME type
# created by CrossOver in this list.
sub read_mime_db($)
{
    my ($self)=@_;
    return if ($self->{read_mime_db});
    $self->{read_mime_db}=1;

    my $handler=Xml2MimeDB->new($self);
    if ($ENV{CX_TINYSAXDEBUG})
    {
        # No need to load those unless we are going to use them
        eval "use CXTinySAXMultiplexer; use CXTinySAXLog;";
        $handler=CXTinySAXMultiplexer->new([CXTinySAXLog->new(), $handler]);
    }

    foreach my $dir (@{$self->{xdg_dirs}})
    {
        my $pkg_dir="$dir/mime/packages";
        next if (!-d $pkg_dir);
        my $dh;
        if (!opendir($dh, $pkg_dir))
        {
            cxlog("unable to open the '$pkg_dir' directory: $!\n");
            next;
        }
        foreach my $dentry (readdir $dh)
        {
            next if ($dentry =~ /^cxassoc-/); # Ignore CrossOver MIME types
            next if ($dentry !~ /\.xml$/);
            if (-f "$pkg_dir/$dentry")
            {
                my $start=CXLog::cxtime();
                CXTinySAX::parse_file($handler, "$pkg_dir/$dentry");
                cxlog("parsing took ", CXLog::cxtime()-$start, " seconds\n");
            }
        }
        closedir($dh);
    }
}


#####
#
# Reading and writing the CrossOver XML MIME file
#
#####

{
    package ReadCXMimes;
    use CXTinySAXBase;
    use base "CXTinySAXBase";
    use CXLog;
    use CXUtils;

    sub new($$)
    {
        my ($class, $xdg)=@_;
        my $self={
            xdg => $xdg,
            tag => ""
        };
        bless $self, $class;
        return undef if (!$self->init());
        return $self;
    }

    sub encoding($$)
    {
        my ($self, $encoding)=@_;
        if ($encoding =~ /^utf-?8$/i)
        {
            delete $self->{recode};
        }
        else
        {
            cxlog("re-encode from '$encoding' to 'utf8'\n");
            $self->{encoding}=$encoding;
        }
        return 1;
    }

    sub recode($$)
    {
        my ($self, $str)=@_;
        if ($self->{encoding})
        {
            require CXRecode;
            $str=CXRecode::from_to($str, $self->{encoding}, "utf8");
        }
        return $str;
    }

    sub start_element($$$)
    {
        my ($self, $element, $attributes)=@_;
        if ($element eq "mime-type" and $attributes->{type})
        {
            my $mimetype=$self->recode($attributes->{type});
            my $cxmime=$self->{xdg}->{cxmimes}->{$mimetype};
            if (defined $cxmime)
            {
                cxlog("'$mimetype' has already been found!\n");
            }
            else
            {
                $cxmime={ mimetype => $mimetype };
                $self->{xdg}->{cxmimes}->{$mimetype}=$cxmime;
            }
            $self->{cxmime}=$cxmime;
        }
        elsif (!$self->{cxmime})
        {
            # Without a current cxmime object there is nothing to do
        }
        elsif ($element eq "glob" and $attributes->{pattern})
        {
            my $extension=$self->recode($attributes->{pattern});
            $extension =~ s/^\*\.//;
            $extension =~ tr/A-Z/a-z/; # extensions are case-insensitive in XDG
            $self->{cxmime}->{extensions}->{$extension}=1;

        }
        elsif ($element eq "comment")
        {
            $self->{tag}="comment";
            $self->{lang}=$self->recode($attributes->{"xml:lang"} || "");
        }
        elsif ($element eq "alias" and $attributes->{type})
        {
            cxlog("unexpected '$attributes->{type}' alias found for '$self->{cxmime}->{mimetype}'\n");
        }

        return 1;
    }

    sub end_element($$)
    {
        my ($self, $element)=@_;
        delete $self->{cxmime} if ($element eq "mime-type");
        $self->{tag}="";
        return 1;
    }

    sub cdata($$$)
    {
        my ($self, $element, $cdata)=@_;
        if ($self->{tag} eq "comment")
        {
            $self->{cxmime}->{comments}->{$self->{lang}}=$cdata;
        }
        return 1;
    }

    sub comment($$)
    {
        my ($self, $comment)=@_;
        if ($comment =~ s/^X-Created-By\s*=\s*//)
        {
            map { $self->{cxmime}->{apps}->{$_}=1 } split /;+/, $comment;
        }
        return 1;
    }
}

sub read_cxmimes($)
{
    my ($self)=@_;
    return if (exists $self->{cxmimes});

    my $handler=ReadCXMimes->new($self);
    if ($ENV{CX_TINYSAXDEBUG})
    {
        # No need to load those unless we are going to use them
        eval "use CXTinySAXMultiplexer; use CXTinySAXLog;";
        $handler=CXTinySAXMultiplexer->new([CXTinySAXLog->new(), $handler]);
    }

    my $filename="$self->{xdg_dir}/mime/packages/cxassoc-$self->{tag}.xml";
    my $start=CXLog::cxtime();
    CXTinySAX::parse_file($handler, $filename);
    cxlog("parsing took ", CXLog::cxtime()-$start, " seconds\n");
}

sub save_cxmimes($)
{
    my ($self)=@_;
    return 1 if (!$self->{modified_mime} or !exists $self->{cxmimes});

    my $filename="$self->{xdg_dir}/mime/packages/cxassoc-$self->{tag}.xml";
    if (!%{$self->{cxmimes}})
    {
        if (-f $filename)
        {
            cxlog("Deleting '$filename'\n");
            if (!unlink $filename)
            {
                cxwarn("unable to delete '$filename': $!\n");
                return 0;
            }
        }
        return 1;
    }

    my $dir=cxdirname($filename);
    if (!cxmkpath($dir))
    {
        cxerr("unable to create the '$dir' directory: $@\n");
        return 0;
    }

    my $fh;
    if (!open($fh, ">", $filename))
    {
        cxerr("unable to open '$filename' for writing: $!\n");
        return 0;
    }

    cxlog("Saving '$filename'\n");
    print $fh "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    print $fh "<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n";
    foreach my $mimetype (sort keys %{$self->{cxmimes}})
    {
        my $cxmime=$self->{cxmimes}->{$mimetype};
        print $fh "  <mime-type type=\"",
                  CXTinySAX::mangle_attribute($mimetype), "\">\n";
        print $fh "    <!-- X-Created-By=",
                  CXTinySAX::mangle_cdata(join(";", keys %{$cxmime->{apps}})),
                  " -->\n";
        if (defined $cxmime->{icon})
        {
            print $fh "    <generic-icon name=\"$cxmime->{icon}\"/>\n";
        }
        foreach my $lang (sort keys %{$cxmime->{comments}})
        {
            my $attribute="";
            if ($lang ne "")
            {
                $attribute=" xml:lang=\"" .
                      CXTinySAX::mangle_attribute($lang) . "\"";
            }
            print $fh "    <comment$attribute>",
                      CXTinySAX::mangle_cdata($cxmime->{comments}->{$lang}),
                      "</comment>\n";
        }
        foreach my $extension (sort keys %{$cxmime->{extensions}})
        {
            print $fh "    <glob pattern=\"*.",
                      CXTinySAX::mangle_cdata($extension), "\"/>\n";
        }
        print $fh "  </mime-type>\n";
    }
    print $fh "</mime-info>\n";
    close($fh);
    return 1;
}


#####
#
# MIME icon creation and deletion
#
#####


sub create_icon($$$)
{
    # Notes:
    # - XDG used to provide no way to specify an icon for a given MIME type
    #   in its XML files.
    # - Now it's possible to specify an icon using either the <generic-icon>
    #   or the <icon> tags, but this breaks older GNOME versions (they use a
    #   validating XML parser). So it's best to avoid them.
    # - However GNOME will automatically use a file called:
    #   <root>/icons/hicolor/48x48/mimetypes/gnome-mime-<mimetype>.xpm
    # - Then we need to touch the hicolor directory to get GNOME to notice the
    #   new icons (see in finalize()).
    # - When KDE started using XDG MIME types, it looked for an icon by the
    #   same name except for the 'gnome-mime-' prefix.
    # - Since the icon name is fixed we cannot use it to identify our icons
    #   or isolate the bottles icons from each other. So we create them as
    #   symbolic links pointing to a file which name contains the bottle tag.
    #   Of course, that second file turns out to be a symbolic link to the
    #   real icon.
    # - All XDG implementations seem to support XPM icons.
    # - GNOME will also use gnome-mime-<mimetype>.xpm files placed in
    #   <root>/pixmaps but only for system-wide associations.
    my ($self, $mime, $mimetype)=@_;
    $self->delete_icon($mimetype);

    my $ext=$mime->{icon} =~ /\.xpm$/i ? ".xpm" : ".png";
    my $link="$self->{desktopdata}/cxassoc/tagged_icons/$self->{tag}." .
             mangle_string($mimetype) . $ext;
    cxlog("Creating '$link'\n");
    my $dir=cxdirname($link);
    if (!cxmkpath($dir))
    {
        cxlog("unable to create the '$dir' directory: $@\n");
    }

    if (!$self->{ro_desktopdata} or !-l $link)
    {
        my $ref_icon=$mime->{icon};
        $ref_icon =~ s!^\Q$ENV{WINEPREFIX}/\E!../../../! if (defined $ENV{WINEPREFIX});
        if (!symlink $ref_icon, $link)
        {
            cxlog("unable to symlink '$link' to '$ref_icon': $!\n");
        }
    }

    my $icon_dir="$self->{xdg_dir}/icons/hicolor/48x48/mimetypes";
    if (!cxmkpath($icon_dir))
    {
        cxlog("unable to create the '$icon_dir' directory: $@\n");
    }
    my $xdg_link="$icon_dir/$self->{tag}_". mangle_string($mimetype) . $ext;
    cxlog("Creating '$xdg_link'\n");
    if (!symlink $link, $xdg_link)
    {
        cxlog("unable to symlink '$xdg_link' to '$link': $!\n");
    }
}

sub delete_icon($$)
{
    my ($self, $mimetype)=@_;

    for my $ext (".xpm", ".png")
    {
        for my $rawdir (@{$self->{xdg_dirs}})
        {
            my $dir="$self->{destdir}$rawdir";
            next if (!-d $dir);
            if (!-w _)
            {
                cxlog("delete_icon: skipping read-only '$dir' directory\n");
                next;
            }
            my $icon_dir="$dir/icons/hicolor/48x48/mimetypes";
            my $icon=$mimetype;
            $icon =~ s!/!-!g;
            for my $prefix ("", "gnome-mime-")
            {
                my $xdg_link="$icon_dir/$prefix$icon$ext";
                if (-l $xdg_link and readlink($xdg_link) =~ m!/$self->{tag}\.!)
                {
                    cxlog("Deleting '$xdg_link'\n");
                    if (!unlink $xdg_link)
                    {
                        cxlog("unable to delete '$xdg_link': $!\n");
                    }
                    $self->{collect_icon_dir}->{$dir}=1;
                }
            }
        }

        my $link="$self->{desktopdata}/cxassoc/tagged_icons/$self->{tag}." .
                 mangle_string($mimetype) . $ext;
        if (!$self->{ro_desktopdata} and -l $link)
        {
            cxlog("Deleting '$link'\n");
            if (!unlink $link)
            {
                cxlog("unable to delete '$link': $!\n");
            }
        }
    }
}

sub get_icon_files($$)
{
    my ($self, $mimetype)=@_;

    my @files;
    my $icon=$mimetype;
    $icon =~ s!/!-!g;
    for my $ext (".xpm", ".png")
    {
        for my $rawdir (@{$self->{xdg_dirs}})
        {
            my $icon_dir="$self->{destdir}$rawdir/icons/hicolor/48x48/mimetypes";
            for my $prefix ("", "gnome-mime-")
            {
                my $xdg_link="$icon_dir/$prefix$icon$ext";
                if (-l $xdg_link and readlink($xdg_link) =~ m!/$self->{tag}\.!)
                {
                    push @files, $xdg_link;
                }
            }
        }
        my $link="$self->{desktopdata}/cxassoc/tagged_icons/$self->{tag}." .
                 mangle_string($mimetype) . $ext;
        push @files, $link if (-l $link);
    }
    return \@files;
}

sub removeall_icon($$)
{
    my ($self, $pattern)=@_;

    my $pat=$pattern;
    $pat="$pat.*" if ($pat !~ s/\$$//);
    for my $rawdir (@{$self->{xdg_dirs}})
    {
        my $dir="$self->{destdir}$rawdir";
        next if (!-d $dir);
        if (!-w _)
        {
            cxlog("removeall_icon: skipping read-only '$dir' directory\n");
            next;
        }
        my $icon_dir="$dir/icons/hicolor/48x48/mimetypes";
        if (opendir(my $dh, $icon_dir))
        {
            foreach my $dentry (readdir $dh)
            {
                next if ($dentry =~ /^\.\.?$/);
                $dentry="$icon_dir/$dentry";
                if (-l $dentry and readlink($dentry) =~ m!/$pat\..*\.(?:png|xpm)$!)
                {
                    cxlog("Deleting '$dentry'\n");
                    if (!unlink $dentry)
                    {
                        cxerr("unable to delete '$dentry': $!\n");
                    }
                    $self->{collect_icon_dir}->{$dir}=1;
                }
            }
            closedir($dh);
        }
        elsif (-d $icon_dir)
        {
            cxlog("unable to open the '$icon_dir' directory: $!\n");
        }
        # Linux distributions ship empty icon directories so don't garbage
        # collect them.
    }

    if (!$self->{ro_desktopdata})
    {
        if ($self->{tag} and $self->{tag} =~ /^$pattern/)
        {
            my $dir="$self->{desktopdata}/cxassoc/tagged_icons";
            if (-d $dir)
            {
                cxlog("Deleting the '$dir' directory\n");
                require File::Path;
                if (!File::Path::rmtree($dir))
                {
                    cxerr("unable to delete the '$dir' directory: $!\n");
                }
            }
            CXUtils::garbage_collect_subdirs($self->{desktopdata}, "/cxassoc", 1);
        }
        else
        {
            require CXBottle;
            CXBottle::removeall_desktopdata_dirs($pattern, "/cxassoc/tagged_icons");
        }
    }
}


#####
#
# MIME type creation and deletion
#
#####

sub create_mime($$$$$$)
{
    my ($self, $domain, $massoc, $mime, $mimetype, $extensions)=@_;
    $self->read_cxmimes();

    my $cxmime=$self->{cxmimes}->{$mimetype} || {};
    if (!$cxmime->{created})
    {
        cxlog("Creating '$mimetype'\n");
        # Recreate the MIME type from scratch...
        delete $cxmime->{icon};
        delete $cxmime->{comments};
        delete $cxmime->{extensions};

        $cxmime->{mimetype}=$mimetype;
        CXAssoc::setup_from_best_eassoc($mime);
        $cxmime->{icon}="$self->{tag}_". mangle_string($mimetype);
        my $description=$mime->{description} || $mimetype;
        $description.=" (" . $self->id() . ")" if ($ENV{CX_TAGALL});
        $cxmime->{comments}->{""}=$description;
        if ($mime->{localize})
        {
            my $oldlang=CXUtils::cxgetlang();
            my $oldencoding=CXUtils::cxsetencoding("UTF-8");
            foreach my $locale (CXUtils::get_supported_locales())
            {
                CXUtils::cxsetlang($locale);
                my $description=cxgettext($mime->{description});
                if ($description ne $mime->{description})
                {
                    $description.=" (" . $self->id() . ")" if ($ENV{CX_TAGALL});
                    $cxmime->{comments}->{$locale}=$description;
                }
            }
            CXUtils::cxsetlang($oldlang);
            CXUtils::cxsetencoding($oldencoding);
        }
        map { $cxmime->{extensions}->{$_}=1 } @$extensions;

        $self->{cxmimes}->{$mimetype}=$cxmime;
        $cxmime->{created}=1;

        $self->create_icon($mime, $mimetype);
    }
    else
    {
        cxlog("Tagging  '$mimetype'\n");
    }
    $cxmime->{apps}->{$massoc->{id}}=1;

    $self->{modified_mime}=1;
    return 1;
}

sub query_mime($$$$$)
{
    my ($self, $domain, $massoc, $mimetype, $extensions)=@_;
    $self->read_cxmimes();

    my $cxmime=$self->{cxmimes}->{$mimetype};
    return 0 if (!$cxmime->{apps}->{$massoc->{id}});
    foreach my $ext (@$extensions)
    {
        return 0 if (!$cxmime->{extensions}->{$ext});
    }

    return 1;
}

sub get_mime_files($$$$$)
{
    my ($self, $domain, $massoc, $mimetype, $extensions)=@_;

    my @files;
    for my $rawdir (@{$self->{xdg_dirs}})
    {
        my $filename="$self->{destdir}$rawdir/mime/packages/cxassoc-$self->{tag}.xml";
        push @files, $filename if (-f $filename);
    }
    push @files, @{$self->get_icon_files($mimetype)};
    return \@files;
}

sub untag_mime($$$$)
{
    my ($self, $domain, $massoc, $mimetype)=@_;
    $self->read_cxmimes();

    my $in_use;
    cxlog("Untagging '$mimetype'\n");
    my $cxmime=$self->{cxmimes}->{$mimetype};
    if (defined $cxmime)
    {
        delete $cxmime->{apps}->{$massoc->{id}};
        if (!%{$cxmime->{apps}})
        {
            # No one is using this MIME type anymore
            delete $self->{cxmimes}->{$mimetype};
        }
        else
        {
            $in_use=1;
        }
        $self->{modified_mime}=1;
    }
    $self->delete_icon($mimetype) if (!$in_use);

    return 1;
}


#####
#
# Association desktop file helper functions
#
#####

sub create_association($$$$)
{
    my ($self, $massoc, $adata, $extensions)=@_;

    # If KDEXDG is in use, then we must also take into account its MIME types
    if ($massoc->{_kdexdg_all_mimes})
    {
        foreach my $mimetype (keys %{$massoc->{_kdexdg_all_mimes}})
        {
            if (!$adata->{all_mimes}->{$mimetype} and
                !$self->ignore_mime_alias($massoc, $adata, $mimetype))
            {
                $adata->{all_mimes}->{$mimetype}=1;
            }
        }
        cxlog("  after kdexdg-ca: ", join(" ", sort keys %{$adata->{all_mimes}}), "\n");
    }

    my $filename="$self->{xdg_dir}/applications";
    if (!cxmkpath($filename))
    {
        cxerr("unable to create the '$filename' directory: $@\n");
        return 0;
    }

    require CXRWConfig;
    my $desktop=CXRWConfig->new(undef, "xdg", "");
    $filename.="/cxassoc-$self->{tag}:$massoc->{id}.desktop";
    cxlog("Creating '$filename'\n");
    $desktop->set_filename($filename);
    my $section=$desktop->append_section("Desktop Entry");
    $section->set("Encoding", "UTF-8");
    $section->set("Type", "Application");
    $section->set("X-Created-By", $self->{tag});
    $section->set("NoDisplay", "true");

    CXAssoc::setup_from_best_eassoc($massoc);
    $section->set("Icon", $massoc->{icon});

    my $name=$massoc->{appname};
    if ($massoc->{verb})
    {
        CXAssoc::compute_verb_name($massoc);
        if ($massoc->{verbname})
        {
            # Notes:
            # - With the verb name coming last, its placement is wrong for
            #   defining a keyboard shortcut.
            # - KDE creates a keyboard shortcut when it finds the VerbName's
            #   ampersand, but GNOME just leaves it in the string. This means
            #   if the string has 'real' ampersands, the first one will be
            #   turned into a keyboard shortcut by KDE while it will look ok
            #   in GNOME. But if we double it so it looks ok in KDE, then it
            #   is in GNOME it will look wrong.
            $name.=" (" . CXAssoc::remove_accelerators($massoc->{verbname}) . ")";
        }
    }
    my $creator;
    if ($ENV{CX_TAGALL})
    {
        $creator=" (" . ($massoc->{_kdexdg_id} || "") . $self->id() . ")";
        $name.=$creator;
    }
    $section->set("Name", $name);
    if ($massoc->{localize} or $massoc->{stdverbname})
    {
        my $oldlang=CXUtils::cxgetlang();
        my $oldencoding=CXUtils::cxsetencoding("UTF-8");
        my $std_verb_names=CXAssoc::std_verb_names();
        foreach my $locale (CXUtils::get_supported_locales())
        {
            CXUtils::cxsetlang($locale);
            my $appname=$massoc->{appname};
            $appname=cxgettext($appname) if ($massoc->{localize});
            my $verbname=$std_verb_names->{$massoc->{verb}};
            $verbname=cxgettext($verbname) if ($massoc->{localize} or $massoc->{stdverbname});
            if ($appname ne $massoc->{appname} or $verbname ne $std_verb_names->{$massoc->{verb}})
            {
                my $name="$appname (" . CXAssoc::remove_accelerators($verbname) . ")";
                $name.=$creator if ($creator);
                $section->set("Name[$locale]", $name);
            }
        }
        CXUtils::cxsetlang($oldlang);
        CXUtils::cxsetencoding($oldencoding);
    }
    $section->set("GenericName", $massoc->{genericname}) if ($massoc->{genericname});
    if ($massoc->{genericname} and $massoc->{localize})
    {
        my $oldlang=CXUtils::cxgetlang();
        my $oldencoding=CXUtils::cxsetencoding("UTF-8");
        foreach my $locale (CXUtils::get_supported_locales())
        {
            CXUtils::cxsetlang($locale);
            my $genericname=cxgettext($massoc->{genericname});
            if ($genericname ne $massoc->{genericname})
            {
                $section->set("GenericName[$locale]", $genericname);
            }
        }
        CXUtils::cxsetlang($oldlang);
        CXUtils::cxsetencoding($oldencoding);
    }

    # The desktop file format specifies that percents must be doubled.
    my $exec=$massoc->{command};
    $exec =~ s/%/%%/g;
    $section->set("Exec", "$exec \%u");
    $section->set("Terminal", "false");
    $section->set("MimeType", join(";", (sort keys %{$adata->{all_mimes}}), ""));

    # KDE (up to 4.2.2) ignores the defaults.list file,
    # forcing us to use the InitialPreference field instead.
    my $preference=($massoc->{mode} eq "default") ? 10 : 1;
    $section->set("InitialPreference", $preference);

    if (!$desktop->save())
    {
        cxerr("unable to save '$filename': $!\n");
        return 0;
    }
    $self->{modified_desktop}=1;
    return 1;
}

sub query_association($$$$)
{
    my ($self, $massoc, $adata, $state)=@_;

    # If KDEXDG is in use, then we must also take into account its MIME types
    if ($massoc->{_kdexdg_all_mimes})
    {
        foreach my $mimetype (keys %{$massoc->{_kdexdg_all_mimes}})
        {
            if (!$adata->{all_mimes}->{$mimetype} and
                !$self->ignore_mime_alias($massoc, $adata, $mimetype))
            {
                $adata->{all_mimes}->{$mimetype}=1;
            }
        }
        cxlog("  after kdexdg-qa: ", join(" ", sort keys %{$adata->{all_mimes}}), "\n");
    }

    my $found;
    for my $rawdir (@{$self->{xdg_dirs}})
    {
        my $dir="$self->{destdir}$rawdir";
        if (-f "$dir/applications/cxassoc-$self->{tag}:$massoc->{id}.desktop")
        {
            $found=1;
            last;
        }
    }
    return $state if (!$found);

    require CXRWConfig;
    my $filename="$self->{xdg_dir}/applications/cxassoc-$self->{tag}:$massoc->{id}.desktop";
    my $desktop=CXRWConfig->new($filename, "xdg", "");
    cxlog("Checking '$filename'\n");

    my $section=$desktop->get_section("Desktop Entry");
    return $state if (!$section);

    my $str=$section->get("MimeType", "");
    my @mimes=split /;+/, $str;
    return $state if (CXAssoc::compare_sets(\@mimes, $adata->{all_mimes}));

    # Dedouble percents before we check the command
    $str=$section->get("Exec", "");
    $str =~ s/%%/%/g;
    return $state if ($str !~ /\Q$massoc->{cmdbase}\E(?:[^:]|$)/);

    return "alternative";
}

sub get_association_files($$$$)
{
    my ($self, $massoc, $adata, $state)=@_;

    my @files;
    for my $rawdir (@{$self->{xdg_dirs}})
    {
        my $filename="$self->{destdir}$rawdir/applications/cxassoc-$self->{tag}:$massoc->{id}.desktop";
        push @files, $filename if (-f $filename);
    }
    return \@files;
}

sub delete_association($$$)
{
    my ($self, $massoc)=@_;

    $self->{modified_desktop}=1;
    my $success=1;
    for my $rawdir (@{$self->{xdg_dirs}})
    {
        my $dir="$self->{destdir}$rawdir";
        next if (!-d $dir);
        if (!-w _)
        {
            cxlog("delete_asociation: skipping read-only '$dir' directory\n");
            next;
        }
        my $filename="$dir/applications/cxassoc-$self->{tag}:$massoc->{id}.desktop";
        require CXRWConfig;
        CXRWConfig::uncache_file($filename);
        if (-f $filename)
        {
            cxlog("Deleting '$filename'\n");
            if (!unlink $filename)
            {
                cxerr("unable to delete '$filename': $!\n");
                $success=0;
            }
        }
    }
    return $success;
}


#####
#
# defaults.list file helper functions
#
#####

sub read_defaults($$)
{
    my ($self)=@_;
    return if (exists $self->{defaults});

    my $filename="$self->{xdg_dir}/applications/defaults.list";
    require CXRWConfig;
    $self->{def_file}=CXRWConfig->new($filename, "xdg", "");
    $self->{def_section}=$self->{def_file}->append_section("Default Applications");
    foreach my $mimetype (@{$self->{def_section}->get_field_list()})
    {
        my $value=$self->{def_section}->get($mimetype);
        my $count=1;
        foreach my $app (split /;+/, $value)
        {
            $self->{def_apps}->{$app}->{$mimetype}=$count++;
        }
    }
}

sub create_default($$$)
{
    my ($self, $massoc, $adata)=@_;
    $self->read_defaults();

    # If KDEXDG is in use, then we must also take into account its MIME types
    if ($massoc->{_kdexdg_default_mimes})
    {
        foreach my $mimetype (keys %{$massoc->{_kdexdg_default_mimes}})
        {
            if (!$adata->{default_mimes}->{$mimetype} and
                !$self->ignore_mime_alias($massoc, $adata, $mimetype))
            {
                $adata->{default_mimes}->{$mimetype}=1;
            }
        }
        cxlog("  after kdexdg-cd: ", join(" ", sort keys %{$adata->{default_mimes}}), "\n");
    }

    my $desktop="cxassoc-$self->{tag}:$massoc->{id}.desktop";
    foreach my $mimetype (keys %{$self->{def_apps}->{$desktop}})
    {
        if (!$adata->{default_mimes}->{$mimetype})
        {
            $self->{def_section}->remove($mimetype);
            delete $self->{def_apps}->{$desktop}->{$mimetype};
        }
    }
    if (!%{$self->{def_apps}->{$desktop}})
    {
        delete $self->{def_apps}->{$desktop}; # for finalize()
    }

    foreach my $mimetype (keys %{$adata->{default_mimes}})
    {
        if (($self->{def_apps}->{$desktop}->{$mimetype} || "") ne "1")
        {
            my @list=($desktop);
            my $str=$self->{def_section}->get($mimetype, "");
            my $count=2;
            foreach my $item (split /;+/, $str)
            {
                if ($item ne $desktop)
                {
                    push @list, $item;
                    $self->{def_apps}->{$item}->{$mimetype}=$count++;
                }
            }
            $self->{def_apps}->{$desktop}->{$mimetype}=1;
            $self->{def_section}->set($mimetype, join(";", @list));
        }
    }

    return 1;
}

sub query_default($$$$)
{
    my ($self, $massoc, $adata, $state)=@_;
    return "alternative" if (!%{$adata->{default_mimes}});
    $self->read_defaults();

    # If KDEXDG is in use, then we must also take into account its MIME types
    if ($massoc->{_kdexdg_default_mimes})
    {
        foreach my $mimetype (keys %{$massoc->{_kdexdg_default_mimes}})
        {
            if (!$adata->{default_mimes}->{$mimetype} and
                !$self->ignore_mime_alias($massoc, $adata, $mimetype))
            {
                $adata->{default_mimes}->{$mimetype}=1;
            }
        }
        cxlog("  after kdexdg-qd: ", join(" ", sort keys %{$adata->{default_mimes}}), "\n");
    }

    my $desktop="cxassoc-$self->{tag}:$massoc->{id}.desktop";
    my $default_mimes=$self->{def_apps}->{$desktop};
    foreach my $mimetype (keys %$default_mimes)
    {
        if (!$adata->{default_mimes}->{$mimetype})
        {
            cxlog("extra MIME type: $mimetype\n");
            return "alternative";
        }
    }
    foreach my $mimetype (keys %{$adata->{default_mimes}})
    {
        if (($default_mimes->{$mimetype} || "") ne "1")
        {
            cxlog("$desktop missing or not first in list for $mimetype\n");
            return "alternative";
        }
    }

    return "default";
}

sub get_default_files($$$$)
{
    my ($self, $massoc, $adata, $state)=@_;
    # The defaults.list file is not specific to this bottle and thus
    # must not be packaged with it.
    return [];
}

sub delete_default($$)
{
    my ($self, $massoc)=@_;
    $self->read_defaults();

    my $desktop="cxassoc-$self->{tag}:$massoc->{id}.desktop";
    foreach my $mimetype (keys %{$self->{def_apps}->{$desktop}})
    {
        my $str=$self->{def_section}->get($mimetype, "");
        if ($str eq $desktop)
        {
            $self->{def_section}->remove($mimetype);
        }
        else
        {
            my @list=grep !/^$desktop$/, split /;+/, $str;
            $self->{def_section}->set($mimetype, join(";", @list));
        }
    }
    delete $self->{def_apps}->{$desktop};
    return 1;
}


#####
#
# Main
#
#####

sub detect($$$$)
{
    my ($class, $cxoptions, $cxconfig, $gui_info)=@_;
    return () if (!$gui_info->{xdg_mime_on} or
                  !$gui_info->{xdg_preferred_data});

    my $self={
        tag            => $cxoptions->{tag},
        winexts        => $cxoptions->{winexts},
        winmimes       => $cxoptions->{winmimes},
        mimealiases    => $cxoptions->{mimealiases},
        mimeignorelist => $cxoptions->{mimeignorelist},
        massocs        => $cxoptions->{massocs},
        destdir        => $cxoptions->{destdir},
        desktopdata    => $cxoptions->{desktopdata},
        ro_desktopdata => $cxoptions->{ro_desktopdata},
        id             => $gui_info->{xdg_preferred_data},
        xdg_dir        => "$cxoptions->{destdir}$gui_info->{xdg_preferred_data}",
        xdg_global_dir => "$cxoptions->{destdir}$gui_info->{xdg_global_data}",
        do_assoc       => 1,
        do_default     => 1,
        kde_mime_path  => $gui_info->{kde_mime_path},
    };
    $self->{xdg_dirs}=[grep {$_ ne ""} split /:+/, $gui_info->{xdg_data_dirs}];
    if ($gui_info->{preferred_scope} eq "private")
    {
        # xdg_data_dirs only contains directories for the managed scope so
        # xdg_preferred_data must be added to the list manually.
        unshift @{$self->{xdg_dirs}}, $gui_info->{xdg_preferred_data};
    }
    bless $self, $class;

    return ($self);
}

sub id($)
{
    my ($self)=@_;
    my $id="CXAssocXDG/$self->{id}";
    $id =~ s%/+%/%g;
    return $id;
}

sub preinstall($$)
{
    my ($self, $massoc)=@_;
    return $self->collect_unix_extensions($massoc);
}

sub install($$)
{
    my ($self, $massoc)=@_;
    return $self->action($self, $massoc, "install");
}

sub query($$)
{
    my ($self, $massoc)=@_;

    if (!$massoc)
    {
        my $id=$self->id();
        return {default     => $id,
                alternative => $id,
                mime        => $id,
                partial     => $id};
    }
    return $self->action($self, $massoc, "query");
}

sub get_files($$)
{
    my ($self, $massoc)=@_;
    return $self->action($self, $massoc, "getfiles");
}

sub uninstall($$)
{
    my ($self, $massoc)=@_;
    return $self->action($self, $massoc, "uninstall");
}

sub removeall_kde_mimes($$)
{
    my ($self, $domain, $pattern)=@_;
    # Until CrossOver 14.0.3 CXAssocXDG usually created KDE MIME types through
    # CXassocKDEXDG. It no longer does but still has to clean those up.

    require CXConfig;
    my @dirs=split /:+/, $self->{kde_mime_path};
    while (@dirs)
    {
        my $dir=$self->{destdir} . shift @dirs;
        next if (!-w $dir);

        my $rmdir=1;
        if (opendir(my $dh, $dir))
        {
            foreach my $dentry (readdir $dh)
            {
                next if ($dentry =~ /^\.\.?$/);
                my $path="$dir/$dentry";
                if (-d $path)
                {
                    push @dirs, $path;
                    next;
                }
                next if ($dentry !~ /\.desktop$/);

                my $cxmime=CXConfig->new($path);
                my $created_by=$cxmime->get("Desktop Entry", "X-Created-By", "");
                cxlog("  created_by=[$created_by]\n");
                next if ($created_by ne "CrossOver");

                cxlog("Deleting '$path'\n");
                if (!unlink $path)
                {
                    cxerr("unable to delete '$path': $!\n");
                }
                $rmdir=1;
            }
            closedir($dh);

            # Try to delete the parent directory to limit accumulation of cruft
            rmdir $dir if ($rmdir);
        }
    }
}

sub removeall($$)
{
    my ($self, $pattern)=@_;
    my $pat;

    # Scan the MIME database for our MIME types
    if ($pattern eq "legacy")
    {
        $pat=CXUtils::get_product_id() . "-app-\\d{5}";
    }
    else
    {
        $pat="cxassoc-$pattern";
        $pat="$pat.*" if ($pat !~ s/\$$//);
        $self->removeall_kde_mimes();
    }
    foreach my $rawdir (@{$self->{xdg_dirs}})
    {
        my $dir="$self->{destdir}$rawdir";
        next if (!-d $dir);
        if (!-w _)
        {
            cxlog("removeall1: skipping read-only '$dir' directory\n");
            next;
        }
        if (CXUtils::delete_files("$dir/mime/packages", "^$pat\\.xml\$") > 0)
        {
            $self->{modified_mime}=1;
        }
    }
    $self->removeall_icon($pattern);

    # Scan the application database for our associations
    # Scan the MIME database for our MIME types
    if ($pattern eq "legacy")
    {
        $pat=CXUtils::get_product_id() . "-app-\\d{5}";
    }
    else
    {
        $pat=$pattern;
        $pat="$pat\[^:]*" if ($pat !~ s/\$$//);
        $pat="cxassoc-$pat:.*";
    }
    foreach my $rawdir (@{$self->{xdg_dirs}})
    {
        my $dir="$self->{destdir}$rawdir";
        next if (!-d $dir);
        if (!-w _)
        {
            cxlog("removeall2: skipping read-only '$dir' directory\n");
            next;
        }
        if (CXUtils::delete_files("$dir/applications", "^$pat\\.desktop\$") > 0)
        {
            $self->{modified_desktop}=1;
        }
    }

    # Scan the defaults database for our associations
    $self->read_defaults();
    foreach my $desktop (keys %{$self->{def_apps}})
    {
        if ($desktop =~ /^$pat\.desktop$/)
        {
            foreach my $mimetype (keys %{$self->{def_apps}->{$desktop}})
            {
                my $str=$self->{def_section}->get($mimetype, "");
                if ($str eq $desktop)
                {
                    $self->{def_section}->remove($mimetype);
                }
                else
                {
                    my @list=grep !/^$desktop$/, split /;+/, $str;
                    $self->{def_section}->set($mimetype, join(";", @list));
                }
            }
            delete $self->{def_apps}->{$desktop};
        }
    }

    return 1;
}

sub finalize($)
{
    my ($self)=@_;
    my $rc=1;

    if ($self->{def_file})
    {
        my $filename=$self->{def_file}->get_filename();
        if (!%{$self->{def_apps}} and
            $self->{def_file}->get_section_names() == 1)
        {
            if (-f $filename)
            {
                cxlog("Deleting '$filename'\n");
                if (!unlink $filename)
                {
                    cxwarn("unable to delete '$filename': $!\n");
                    $rc=0;
                }
            }
        }
        elsif (!$self->{def_file}->save())
        {
            cxerr("unable to save '$filename': $!\n");
        }
    }

    if ($self->{modified_mime})
    {
        # {modified_mime} must be set whenever the MIME type xml file is
        # created, modified or deleted. We must then run update-mime-database.
        $rc=0 if (!$self->save_cxmimes());
        if (!$self->{destdir})
        {
            cxsystem("update-mime-database " .
                     shquote_string("$self->{xdg_dir}/mime") . " >/dev/null");
        }
    }
    for my $dir (keys %{$self->{collect_icon_dir}})
    {
        if (-d "$dir/icons/hicolor")
        {
            CXUtils::garbage_collect_subdirs($dir, "/icons/hicolor/48x48/mimetypes", 1);
        }
    }
    # Get XDG to notice our new icons
    my $now=time();
    utime $now, $now, "$self->{xdg_dir}/icons/hicolor";

    if (!$self->{ro_desktopdata} and defined $self->{desktopdata})
    {
        CXUtils::garbage_collect_subdirs($self->{desktopdata},
                                         "/cxassoc/tagged_icons", 1);
    }

    if ($self->{modified_desktop} and !$self->{destdir})
    {
        # {modified_desktop} must be set whenever an association desktop
        # file is created, modified or deleted. We must then run
        # update-desktop-database to regenerate mimeinfo.cache.
        # Note: On SUSE 9.3 update-desktop-database is not in the
        #       PATH when invoked from the RPM postinstall script.
        my $path="$ENV{PATH}:/opt/gnome/bin";
        my $updater=CXUtils::cxwhich($path, "update-desktop-database");
        if (defined $updater)
        {
            my @cmd=($updater, "-q");
            if ($self->{xdg_dir} ne $self->{xdg_global_dir})
            {
                # By default update-desktop-database tries to modify the
                # global files. So in this case we must specify the path
                # explicitly.
                push @cmd, "$self->{xdg_dir}/applications";
            }
            cxsystem(@cmd);
        }
        else
        {
            cxwarn("unable to find 'update-desktop-database' in '$path'\n");
        }
    }

    return $rc;
}

return 1;