1/*
2 * Copyright (C) 2011 Michal Hruby <michal.mhr@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
17 *
18 * Authored by Michal Hruby <michal.mhr@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24  public class LaunchpadPlugin : Object, Activatable, ItemProvider //, Configurable, ActionProvider
25  {
26    public bool enabled { get; set; default = true; }
27
28    private LaunchpadAuthObject? auth_object;
29
30    public void activate ()
31    {
32      //auth_object = new LaunchpadAuthObject ();
33    }
34
35    public void deactivate ()
36    {
37      auth_object = null;
38    }
39
40    public Gtk.Widget create_config_widget ()
41    {
42      var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
43      box.show ();
44
45      var authorize_button = new Gtk.Button.with_label (_("Authorize with Launchpad"));
46      authorize_button.show ();
47      box.pack_start (authorize_button, true, false);
48
49      var spinner = new Gtk.Spinner ();
50      box.pack_start (spinner);
51
52      var label = new Gtk.Label (_("Please press the Finish button once you login to Launchpad with your web browser"));
53      label.set_width_chars (40);
54      label.set_line_wrap (true);
55      var proceed_button = new Gtk.Button.with_label (_("Finish authorization"));
56      box.pack_start (label);
57      box.pack_start (proceed_button, true, false);
58
59      /*
60      HashTable<string, string>? step1_result = null;
61
62      // i'm quite sure this leaks as hell, but it works :)
63      authorize_button.clicked.connect (() => {
64        authorize_button.hide ();
65        spinner.show ();
66        spinner.start ();
67        auth_object.auth_step1.begin ((obj, res) => {
68          // FIXME: handle error
69          step1_result = auth_object.auth_step1.end (res);
70          auth_object.auth_step2 (step1_result.lookup ("oauth_token"));
71          Timeout.add_seconds (5, () => {
72            spinner.hide ();
73            spinner.stop ();
74            label.show ();
75            proceed_button.show ();
76
77            return false;
78          });
79        });
80      });
81
82      proceed_button.clicked.connect (() => {
83        proceed_button.hide ();
84        label.hide ();
85        spinner.show ();
86        spinner.start ();
87        auth_object.auth_step3.begin (step1_result.lookup ("oauth_token"),
88                                      step1_result.lookup ("oauth_token_secret"),
89                                      (obj, res) => {
90          spinner.hide ();
91          try
92          {
93            var step3_result = auth_object.auth_step3.end (res);
94            message ("token: %s", step3_result.lookup ("oauth_token"));
95            message ("token_secret: %s", step3_result.lookup ("oauth_token_secret"));
96
97            label.set_text (_("Successfully authenticated"));
98          }
99          catch (Error e)
100          {
101            label.set_text (_("Authentication failed") + " (%s)".printf (e.message));
102          }
103
104          label.show ();
105        });
106      });
107      */
108
109      return box;
110    }
111
112    private class LaunchpadAuthObject : Object
113    {
114      const string CONSUMER_KEY = "Synapse.LaunchpadPlugin";
115/*
116      private Rest.Proxy proxy;
117
118      protected HashTable<string, string> parse_form_reply (string payload)
119      {
120        var ht = new HashTable<string, string> (str_hash, str_equal);
121
122        string[] parameters = payload.split ("&");
123        foreach (unowned string p in parameters)
124        {
125          string[] parameter = p.split ("=", 2);
126          ht.insert (parameter[0], parameter[1]);
127        }
128
129        return ht;
130      }
131
132      private class Credentials : ConfigObject
133      {
134        public string token { get; set; default = ""; }
135        public string token_secret { get; set; default = ""; }
136      }
137
138      private Credentials creds;
139
140      construct
141      {
142        // make sure we keep a ref to this, otherwise it'll crash when the call
143        // finishes
144        proxy = new Rest.Proxy ("https://launchpad.net/", false);
145
146        creds = ConfigService.get_default ().bind_config (
147          "plugins", "launchpad-plugin", typeof (Credentials)
148        ) as Credentials;
149      }
150
151      public bool is_authenticated ()
152      {
153        return creds.token != "" && creds.token_secret != "";
154      }
155
156      public void get_tokens (out string token, out string token_secret)
157      {
158        token = creds.token;
159        token_secret = creds.token_secret;
160      }
161
162      public async HashTable<string, string> auth_step1 () throws Error
163      {
164        Error? err = null;
165
166        var call = proxy.new_call ();
167
168        call.set_method ("POST");
169        call.set_function ("+request-token");
170        call.add_param ("oauth_consumer_key", CONSUMER_KEY);
171        call.add_param ("oauth_signature_method", "PLAINTEXT");
172        call.add_param ("oauth_signature", "&");
173
174        call.run_async ((call_obj, error, obj) => {
175          err = error;
176          auth_step1.callback ();
177        }, this);
178        yield;
179
180        if (err != null) throw err;
181
182        // the reply should have oauth_token & oauth_token_secret
183        var result = parse_form_reply (call.get_payload ());
184        return result;
185      }
186
187      public void auth_step2 (string oauth_token)
188      {
189        // https://launchpad.net/+authorize-token?oauth_token={oauth_token}
190        Utils.open_uri ("https://launchpad.net/+authorize-token?oauth_token=" + oauth_token);
191      }
192
193      public async HashTable<string, string> auth_step3 (string oauth_token,
194                                                         string token_secret) throws Error
195      {
196        Error? err = null;
197
198        var call = proxy.new_call ();
199        call.set_method ("POST");
200        call.set_function ("+access-token");
201        call.add_param ("oauth_token", oauth_token);
202        call.add_param ("oauth_consumer_key", CONSUMER_KEY);
203        call.add_param ("oauth_signature_method", "PLAINTEXT");
204        call.add_param ("oauth_signature", "&" + token_secret);
205
206        call.run_async ((call_obj, error, obj) => {
207          err = error;
208          auth_step3.callback ();
209        }, this);
210        yield;
211
212        if (err != null) throw err;
213
214        // the reply should have new oauth_token & oauth_token_secret
215        var result = parse_form_reply (call.get_payload ());
216        creds.token = result.lookup ("oauth_token") ?? "";
217        creds.token_secret = result.lookup ("oauth_token_secret") ?? "";
218
219        return result;
220      }
221*/
222    }
223
224    private class LaunchpadObject : UriMatch
225    {
226      public LaunchpadObject (string title, string desc, string uri)
227      {
228        Object (title: title, description: desc,
229                icon_name: ContentType.get_icon ("text/html").to_string (),
230                uri: uri, mime_type: "text/html",
231                file_type: QueryFlags.INTERNET);
232      }
233    }
234
235    static void register_plugin ()
236    {
237      PluginRegistry.get_default ().register_plugin (
238        typeof (LaunchpadPlugin),
239        "Launchpad",
240        _("Find bugs and branches on Launchpad."),
241        "applications-internet",
242        register_plugin
243      );
244    }
245
246    static construct
247    {
248      register_plugin ();
249    }
250
251    private Regex bug_regex;
252    private Regex branch_regex;
253
254    construct
255    {
256      try
257      {
258        bug_regex = new Regex ("(?:bug|lp|#):?\\s*#?\\s*(\\d+)$", RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
259        branch_regex = new Regex ("lp:(~?[a-z]+[+-/_a-z0-9]*)", RegexCompileFlags.OPTIMIZE);
260      }
261      catch (RegexError err)
262      {
263        warning ("Unable to construct regex: %s", err.message);
264      }
265    }
266
267    public bool handles_query (Query q)
268    {
269      return (QueryFlags.INTERNET in q.query_type || QueryFlags.ACTIONS in q.query_type);
270    }
271
272    public async ResultSet? search (Query q) throws SearchError
273    {
274      string? uri = null;
275      string title = null;
276      string description = null;
277      var result = new ResultSet ();
278
279      string stripped = q.query_string.strip ();
280      if (stripped == "") return null;
281
282      MatchInfo mi;
283      if (branch_regex.match (stripped, 0, out mi))
284      {
285        string branch = mi.fetch (1);
286        string[] groups = branch.split ("/");
287        if (groups.length == 1)
288        {
289          // project link (lp:synapse)
290          uri = "https://code.launchpad.net/" + branch;
291          title = _("Launchpad: Bazaar branches for %s").printf (branch);
292          description = uri;
293        }
294        else if (groups.length == 2 && !branch.has_prefix ("~"))
295        {
296          // series link (lp:synapse/0.3)
297          uri = "https://code.launchpad.net/" + branch;
298          title = _("Launchpad: Series %s for Project %s").printf (groups[1], groups[0]);
299          description = uri;
300        }
301        else if (branch.has_prefix ("~"))
302        {
303          // branch link (lp:~mhr3/synapse/lp-plugin)
304          uri = "https://code.launchpad.net/" + branch;
305          title = _("Launchpad: Bazaar branch %s").printf (branch);
306          description = uri;
307        }
308
309        if (uri != null)
310        {
311          result.add (new LaunchpadObject (title, description, uri),
312                      MatchScore.EXCELLENT);
313        }
314      }
315      else if (bug_regex.match (stripped, 0, out mi))
316      {
317        string bug_num = mi.fetch (1);
318
319        uri = "https://bugs.launchpad.net/bugs/" + bug_num;
320        title = _("Launchpad: Bug #%s").printf (bug_num);
321        description = uri;
322        result.add (new LaunchpadObject (title, description, uri),
323                    MatchScore.ABOVE_AVERAGE);
324      }
325
326      q.check_cancellable ();
327      return result;
328    }
329
330    public ResultSet? find_for_match (ref Query query, Match match)
331    {
332      return null;
333    }
334  }
335}
336