1#!/bin/sh
2#---------------------------------------------
3#   xdg-open
4#
5#   Utility script to open a URL in the registered default application.
6#
7#   Refer to the usage() function below for usage.
8#
9#   Copyright 2009-2010, Fathi Boudra <fabo@freedesktop.org>
10#   Copyright 2009-2010, Rex Dieter <rdieter@fedoraproject.org>
11#   Copyright 2006, Kevin Krammer <kevin.krammer@gmx.at>
12#   Copyright 2006, Jeremy White <jwhite@codeweavers.com>
13#
14#   LICENSE:
15#
16#   Permission is hereby granted, free of charge, to any person obtaining a
17#   copy of this software and associated documentation files (the "Software"),
18#   to deal in the Software without restriction, including without limitation
19#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
20#   and/or sell copies of the Software, and to permit persons to whom the
21#   Software is furnished to do so, subject to the following conditions:
22#
23#   The above copyright notice and this permission notice shall be included
24#   in all copies or substantial portions of the Software.
25#
26#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
27#   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
29#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30#   OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31#   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32#   OTHER DEALINGS IN THE SOFTWARE.
33#
34#---------------------------------------------
35
36manualpage()
37{
38cat << _MANUALPAGE
39Name
40
41xdg-open - opens a file or URL in the user's preferred application
42
43Synopsis
44
45xdg-open { file | URL }
46
47xdg-open { --help | --manual | --version }
48
49Description
50
51xdg-open opens a file or URL in the user's preferred application. If a URL is
52provided the URL will be opened in the user's preferred web browser. If a file
53is provided the file will be opened in the preferred application for files of
54that type. xdg-open supports file, ftp, http and https URLs.
55
56xdg-open is for use inside a desktop session only. It is not recommended to use
57xdg-open as root.
58
59Options
60
61--help
62    Show command synopsis.
63--manual
64    Show this manualpage.
65--version
66    Show the xdg-utils version information.
67
68Exit Codes
69
70An exit code of 0 indicates success while a non-zero exit code indicates
71failure. The following failure codes can be returned:
72
731
74    Error in command line syntax.
752
76    One of the files passed on the command line did not exist.
773
78    A required tool could not be found.
794
80    The action failed.
81
82Examples
83
84xdg-open 'http://www.freedesktop.org/'
85
86Opens the Freedesktop.org website in the user's default browser
87
88xdg-open /tmp/foobar.png
89
90Opens the PNG image file /tmp/foobar.png in the user's default image viewing
91application.
92
93_MANUALPAGE
94}
95
96usage()
97{
98cat << _USAGE
99xdg-open - opens a file or URL in the user's preferred application
100
101Synopsis
102
103xdg-open { file | URL }
104
105xdg-open { --help | --manual | --version }
106
107_USAGE
108}
109
110#@xdg-utils-common@
111
112#----------------------------------------------------------------------------
113#   Common utility functions included in all XDG wrapper scripts
114#----------------------------------------------------------------------------
115
116DEBUG()
117{
118  [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0;
119  [ ${XDG_UTILS_DEBUG_LEVEL} -lt $1 ] && return 0;
120  shift
121  echo "$@" >&2
122}
123
124# This handles backslashes but not quote marks.
125first_word()
126{
127    read first rest
128    echo "$first"
129}
130
131#-------------------------------------------------------------
132# map a binary to a .desktop file
133binary_to_desktop_file()
134{
135    search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
136    binary="`which "$1"`"
137    binary="`readlink -f "$binary"`"
138    base="`basename "$binary"`"
139    IFS=:
140    for dir in $search; do
141        unset IFS
142        [ "$dir" ] || continue
143        [ -d "$dir/applications" -o -d "$dir/applnk" ] || continue
144        for file in "$dir"/applications/*.desktop "$dir"/applications/*/*.desktop "$dir"/applnk/*.desktop "$dir"/applnk/*/*.desktop; do
145            [ -r "$file" ] || continue
146            # Check to make sure it's worth the processing.
147            grep -q "^Exec.*$base" "$file" || continue
148            # Make sure it's a visible desktop file (e.g. not "preferred-web-browser.desktop").
149            grep -Eq "^(NoDisplay|Hidden)=true" "$file" && continue
150            command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`"
151            command="`which "$command"`"
152            if [ x"`readlink -f "$command"`" = x"$binary" ]; then
153                # Fix any double slashes that got added path composition
154                echo "$file" | sed -e 's,//*,/,g'
155                return
156            fi
157        done
158    done
159}
160
161#-------------------------------------------------------------
162# map a .desktop file to a binary
163## FIXME: handle vendor dir case
164desktop_file_to_binary()
165{
166    search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
167    desktop="`basename "$1"`"
168    IFS=:
169    for dir in $search; do
170        unset IFS
171        [ "$dir" -a -d "$dir/applications" ] || continue
172        file="$dir/applications/$desktop"
173        [ -r "$file" ] || continue
174        # Remove any arguments (%F, %f, %U, %u, etc.).
175        command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`"
176        command="`which "$command"`"
177        readlink -f "$command"
178        return
179    done
180}
181
182#-------------------------------------------------------------
183# Exit script on successfully completing the desired operation
184
185exit_success()
186{
187    if [ $# -gt 0 ]; then
188        echo "$@"
189        echo
190    fi
191
192    exit 0
193}
194
195
196#-----------------------------------------
197# Exit script on malformed arguments, not enough arguments
198# or missing required option.
199# prints usage information
200
201exit_failure_syntax()
202{
203    if [ $# -gt 0 ]; then
204        echo "xdg-open: $@" >&2
205        echo "Try 'xdg-open --help' for more information." >&2
206    else
207        usage
208        echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info."
209    fi
210
211    exit 1
212}
213
214#-------------------------------------------------------------
215# Exit script on missing file specified on command line
216
217exit_failure_file_missing()
218{
219    if [ $# -gt 0 ]; then
220        echo "xdg-open: $@" >&2
221    fi
222
223    exit 2
224}
225
226#-------------------------------------------------------------
227# Exit script on failure to locate necessary tool applications
228
229exit_failure_operation_impossible()
230{
231    if [ $# -gt 0 ]; then
232        echo "xdg-open: $@" >&2
233    fi
234
235    exit 3
236}
237
238#-------------------------------------------------------------
239# Exit script on failure returned by a tool application
240
241exit_failure_operation_failed()
242{
243    if [ $# -gt 0 ]; then
244        echo "xdg-open: $@" >&2
245    fi
246
247    exit 4
248}
249
250#------------------------------------------------------------
251# Exit script on insufficient permission to read a specified file
252
253exit_failure_file_permission_read()
254{
255    if [ $# -gt 0 ]; then
256        echo "xdg-open: $@" >&2
257    fi
258
259    exit 5
260}
261
262#------------------------------------------------------------
263# Exit script on insufficient permission to write a specified file
264
265exit_failure_file_permission_write()
266{
267    if [ $# -gt 0 ]; then
268        echo "xdg-open: $@" >&2
269    fi
270
271    exit 6
272}
273
274check_input_file()
275{
276    if [ ! -e "$1" ]; then
277        exit_failure_file_missing "file '$1' does not exist"
278    fi
279    if [ ! -r "$1" ]; then
280        exit_failure_file_permission_read "no permission to read file '$1'"
281    fi
282}
283
284check_vendor_prefix()
285{
286    file_label="$2"
287    [ -n "$file_label" ] || file_label="filename"
288    file=`basename "$1"`
289    case "$file" in
290       [a-zA-Z]*-*)
291         return
292         ;;
293    esac
294
295    echo "xdg-open: $file_label '$file' does not have a proper vendor prefix" >&2
296    echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2
297    echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2
298    echo "Use --novendor to override or 'xdg-open --manual' for additional info." >&2
299    exit 1
300}
301
302check_output_file()
303{
304    # if the file exists, check if it is writeable
305    # if it does not exists, check if we are allowed to write on the directory
306    if [ -e "$1" ]; then
307        if [ ! -w "$1" ]; then
308            exit_failure_file_permission_write "no permission to write to file '$1'"
309        fi
310    else
311        DIR=`dirname "$1"`
312        if [ ! -w "$DIR" -o ! -x "$DIR" ]; then
313            exit_failure_file_permission_write "no permission to create file '$1'"
314        fi
315    fi
316}
317
318#----------------------------------------
319# Checks for shared commands, e.g. --help
320
321check_common_commands()
322{
323    while [ $# -gt 0 ] ; do
324        parm="$1"
325        shift
326
327        case "$parm" in
328            --help)
329            usage
330            echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info."
331            exit_success
332            ;;
333
334            --manual)
335            manualpage
336            exit_success
337            ;;
338
339            --version)
340            echo "xdg-open 1.1.0 rc1"
341            exit_success
342            ;;
343        esac
344    done
345}
346
347check_common_commands "$@"
348
349[ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL;
350if [ ${XDG_UTILS_DEBUG_LEVEL-0} -lt 1 ]; then
351    # Be silent
352    xdg_redirect_output=" > /dev/null 2> /dev/null"
353else
354    # All output to stderr
355    xdg_redirect_output=" >&2"
356fi
357
358#--------------------------------------
359# Checks for known desktop environments
360# set variable DE to the desktop environments name, lowercase
361
362detectDE()
363{
364    # see https://bugs.freedesktop.org/show_bug.cgi?id=34164
365    unset GREP_OPTIONS
366
367    if [ -n "${XDG_CURRENT_DESKTOP}" ]; then
368      case "${XDG_CURRENT_DESKTOP}" in
369         GNOME)
370           DE=gnome;
371           ;;
372         KDE)
373           DE=kde;
374           ;;
375         LXDE)
376           DE=lxde;
377           ;;
378         XFCE)
379           DE=xfce
380      esac
381    fi
382
383    if [ x"$DE" = x"" ]; then
384      # classic fallbacks
385      if [ x"$KDE_FULL_SESSION" = x"true" ]; then DE=kde;
386      elif [ x"$GNOME_DESKTOP_SESSION_ID" != x"" ]; then DE=gnome;
387      elif `dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.gnome.SessionManager > /dev/null 2>&1` ; then DE=gnome;
388      elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce;
389      elif xprop -root 2> /dev/null | grep -i '^xfce_desktop_window' >/dev/null 2>&1; then DE=xfce
390      fi
391    fi
392
393    if [ x"$DE" = x"" ]; then
394      # fallback to checking $DESKTOP_SESSION
395      case "$DESKTOP_SESSION" in
396         gnome)
397           DE=gnome;
398           ;;
399         LXDE)
400           DE=lxde;
401           ;;
402         xfce|xfce4)
403           DE=xfce;
404           ;;
405      esac
406    fi
407
408    if [ x"$DE" = x"" ]; then
409      # fallback to uname output for other platforms
410      case "$(uname 2>/dev/null)" in
411        Darwin)
412          DE=darwin;
413          ;;
414      esac
415    fi
416
417    if [ x"$DE" = x"gnome" ]; then
418      # gnome-default-applications-properties is only available in GNOME 2.x
419      # but not in GNOME 3.x
420      which gnome-default-applications-properties > /dev/null 2>&1  || DE="gnome3"
421    fi
422}
423
424#----------------------------------------------------------------------------
425# kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4
426# It also always returns 1 in KDE 3.4 and earlier
427# Simply return 0 in such case
428
429kfmclient_fix_exit_code()
430{
431    version=`LC_ALL=C.UTF-8 kde-config --version 2>/dev/null | grep '^KDE'`
432    major=`echo $version | sed 's/KDE.*: \([0-9]\).*/\1/'`
433    minor=`echo $version | sed 's/KDE.*: [0-9]*\.\([0-9]\).*/\1/'`
434    release=`echo $version | sed 's/KDE.*: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/'`
435    test "$major" -gt 3 && return $1
436    test "$minor" -gt 5 && return $1
437    test "$release" -gt 4 && return $1
438    return 0
439}
440
441# This handles backslashes but not quote marks.
442first_word()
443{
444    read first rest
445    echo "$first"
446}
447
448last_word()
449{
450    read first rest
451    echo "$rest"
452}
453
454open_darwin()
455{
456    open "$1"
457
458    if [ $? -eq 0 ]; then
459        exit_success
460    else
461        exit_failure_operation_failed
462    fi
463}
464
465open_kde()
466{
467    if kde-open -v 2>/dev/null 1>&2; then
468        kde-open "$1"
469    else
470        if [ x"$KDE_SESSION_VERSION" = x"4" ]; then
471            kfmclient openURL "$1"
472        else
473            kfmclient exec "$1"
474            kfmclient_fix_exit_code $?
475        fi
476    fi
477
478    if [ $? -eq 0 ]; then
479        exit_success
480    else
481        exit_failure_operation_failed
482    fi
483}
484
485open_gnome()
486{
487    if gvfs-open --help 2>/dev/null 1>&2; then
488        gvfs-open "$1"
489    else
490        gnome-open "$1"
491    fi
492
493    if [ $? -eq 0 ]; then
494        exit_success
495    else
496        exit_failure_operation_failed
497    fi
498}
499
500open_xfce()
501{
502    exo-open "$1"
503
504    if [ $? -eq 0 ]; then
505        exit_success
506    else
507        exit_failure_operation_failed
508    fi
509}
510
511open_generic_xdg_mime()
512{
513    filetype=`xdg-mime query filetype "$1" | sed "s/;.*//"`
514    default=`xdg-mime query default "$filetype"`
515    if [ -n "$default" ] ; then
516        xdg_user_dir="$XDG_DATA_HOME"
517        [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share"
518
519        xdg_system_dirs="$XDG_DATA_DIRS"
520        [ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/
521
522        for x in `echo "$xdg_user_dir:$xdg_system_dirs" | sed 's/:/ /g'`; do
523            local file
524            # look for both vendor-app.desktop, vendor/app.desktop
525            if [ -r "$x/applications/$default" ]; then
526              file="$x/applications/$default"
527            elif [ -r "$x/applications/`echo $default | sed -e 's|-|/|'`" ]; then
528              file="$x/applications/`echo $default | sed -e 's|-|/|'`"
529            fi
530
531            if [ -r "$file" ] ; then
532                command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`"
533                command_exec=`which $command 2>/dev/null`
534                arguments="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | last_word`"
535                arguments_exec="`echo $arguments | sed -e 's*%[fFuU]*"'"$1"'"*g'`"
536                if [ -x "$command_exec" ] ; then
537                    if echo $arguments | grep -iq '%[fFuU]' ; then
538                        eval $command_exec $arguments_exec
539                    else
540                        eval $command_exec $arguments_exec "$1"
541                    fi
542
543                    if [ $? -eq 0 ]; then
544                        exit_success
545                    fi
546                fi
547            fi
548        done
549    fi
550}
551
552open_generic()
553{
554    # Paths or file:// URLs
555    if (echo "$1" | grep -q '^file://' ||
556        ! echo "$1" | egrep -q '^[a-zA-Z+\.\-]+:'); then
557
558        local file="$1"
559
560        # Decode URLs
561        if echo "$file" | grep -q '^file:///'; then
562            file=${file#file://}
563            file="$(printf "$(echo "$file" | sed -e 's@%\([a-f0-9A-F]\{2\}\)@\\x\1@g')")"
564        fi
565        check_input_file "$file"
566
567        open_generic_xdg_mime "$file"
568
569        if [ -f /etc/debian_version ] &&
570            which run-mailcap 2>/dev/null 1>&2; then
571            run-mailcap --action=view "$file"
572            if [ $? -eq 0 ]; then
573                exit_success
574            fi
575        fi
576
577        if mimeopen -v 2>/dev/null 1>&2; then
578            mimeopen -L -n "$file"
579            if [ $? -eq 0 ]; then
580                exit_success
581            fi
582        fi
583    fi
584
585    IFS=":"
586    for browser in $BROWSER; do
587        if [ x"$browser" != x"" ]; then
588
589            browser_with_arg=`printf "$browser" "$1" 2>/dev/null`
590            if [ $? -ne 0 ]; then
591                browser_with_arg=$browser;
592            fi
593
594            if [ x"$browser_with_arg" = x"$browser" ]; then
595                "$browser" "$1";
596            else eval '$browser_with_arg'$xdg_redirect_output;
597            fi
598
599            if [ $? -eq 0 ]; then
600                exit_success;
601            fi
602        fi
603    done
604
605    exit_failure_operation_impossible "no method available for opening '$1'"
606}
607
608open_lxde()
609{
610    # pcmanfm only knows how to handle file:// urls and filepaths, it seems.
611    if (echo "$1" | grep -q '^file://' ||
612        ! echo "$1" | egrep -q '^[a-zA-Z+\.\-]+:')
613    then
614        local file="$(echo "$1" | sed 's%^file://%%')"
615
616        # handle relative paths
617        if ! echo "$file" | grep -q '^/'; then
618            file="$(pwd)/$file"
619        fi
620
621        pcmanfm "$file"
622
623    else
624        open_generic "$1"
625    fi
626
627    if [ $? -eq 0 ]; then
628        exit_success
629    else
630        exit_failure_operation_failed
631    fi
632}
633
634[ x"$1" != x"" ] || exit_failure_syntax
635
636url=
637while [ $# -gt 0 ] ; do
638    parm="$1"
639    shift
640
641    case "$parm" in
642      -*)
643        exit_failure_syntax "unexpected option '$parm'"
644        ;;
645
646      *)
647        if [ -n "$url" ] ; then
648            exit_failure_syntax "unexpected argument '$parm'"
649        fi
650        url="$parm"
651        ;;
652    esac
653done
654
655if [ -z "${url}" ] ; then
656    exit_failure_syntax "file or URL argument missing"
657fi
658
659detectDE
660
661if [ x"$DE" = x"" ]; then
662    DE=generic
663fi
664
665# if BROWSER variable is not set, check some well known browsers instead
666if [ x"$BROWSER" = x"" ]; then
667    BROWSER=links2:elinks:links:lynx:w3m
668    if [ -n "$DISPLAY" ]; then
669        BROWSER=x-www-browser:firefox:seamonkey:mozilla:epiphany:konqueror:chromium-browser:google-chrome:$BROWSER
670    fi
671fi
672
673case "$DE" in
674    kde)
675    open_kde "$url"
676    ;;
677
678    gnome*)
679    open_gnome "$url"
680    ;;
681
682    xfce)
683    open_xfce "$url"
684    ;;
685
686    lxde)
687    open_lxde "$url"
688    ;;
689
690    generic)
691    open_generic "$url"
692    ;;
693
694    *)
695    exit_failure_operation_impossible "no method available for opening '$url'"
696    ;;
697esac
698