879d2a9abc217221c2f46a92d36e29c045fa6a60
[profile/ivi/rygel.git] / src / rygel / rygel-item-creator.vala
1 /*
2  * Copyright (C) 2010 Nokia Corporation.
3  *
4  * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
5  *
6  * This file is part of Rygel.
7  *
8  * Rygel is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * Rygel is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public License
19  * along with this program; if not, write to the Free Software Foundation,
20  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  */
22
23 using GUPnP;
24 using Gst;
25
26 private errordomain Rygel.ItemCreatorError {
27     PARSE
28 }
29
30 /**
31  * CreateObject action implementation.
32  */
33 internal class Rygel.ItemCreator: GLib.Object, Rygel.StateMachine {
34     private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");
35
36     private const string INVALID_CHARS = "/?<>\\:*|\"";
37
38     // In arguments
39     private string container_id;
40     private string elements;
41
42     private DIDLLiteItem didl_item;
43     private MediaItem item;
44
45     private ContentDirectory content_dir;
46     private ServiceAction action;
47     private DIDLLiteWriter didl_writer;
48     private DIDLLiteParser didl_parser;
49     private Regex title_regex;
50
51     public Cancellable cancellable { get; set; }
52
53     public ItemCreator (ContentDirectory    content_dir,
54                         owned ServiceAction action) {
55         this.content_dir = content_dir;
56         this.cancellable = content_dir.cancellable;
57         this.action = (owned) action;
58         this.didl_writer = new DIDLLiteWriter (null);
59         this.didl_parser = new DIDLLiteParser ();
60         try {
61             var pattern = "[" + Regex.escape_string (INVALID_CHARS) + "]";
62             this.title_regex = new Regex (pattern,
63                                           RegexCompileFlags.OPTIMIZE,
64                                           RegexMatchFlags.NOTEMPTY);
65         } catch (Error error) { assert_not_reached (); }
66     }
67
68     public async void run () {
69         try {
70             this.parse_args ();
71             this.parse_didl ();
72
73             var container = yield this.fetch_container ();
74
75             /* Verify the create class. Note that we always assume
76              * createClass@includeDerived to be false.
77              *
78              * DLNA_ORG.AnyContainer is a special case. We are allowed to
79              * modify the UPnP class to something we support and
80              * fetch_container took care of this already.
81              */
82             if (!container.can_create (this.didl_item.upnp_class) &&
83                 this.container_id != "DLNA_ORG.AnyContainer") {
84                 throw new ContentDirectoryError.BAD_METADATA
85                                         ("Creating of objects with class %s " +
86                                          "is not supported in %s",
87                                          this.didl_item.upnp_class,
88                                          container.id);
89             }
90
91             yield this.create_item_from_didl (container);
92             yield container.add_item (this.item, this.cancellable);
93
94             yield this.wait_for_item (container);
95
96             this.item.serialize (didl_writer, this.content_dir.http_server);
97
98             // Conclude the successful action
99             this.conclude ();
100
101             if (this.container_id == "DLNA.ORG_AnyContainer" &&
102                 this.item.place_holder) {
103                 var queue = ItemRemovalQueue.get_default ();
104
105                 queue.queue (this.item, this.cancellable);
106             }
107         } catch (Error err) {
108             this.handle_error (err);
109         }
110     }
111
112     private void parse_args () throws Error {
113         /* Start by parsing the 'in' arguments */
114         this.action.get ("ContainerID", typeof (string), out this.container_id,
115                          "Elements", typeof (string), out this.elements);
116
117         if (this.elements == null) {
118             throw new ContentDirectoryError.BAD_METADATA
119                                         (_("'Elements' argument missing."));
120         } else if (comment_pattern.match_string (this.elements)) {
121             throw new ContentDirectoryError.BAD_METADATA
122                                         (_("Comments not allowed in XML"));
123         }
124
125         if (this.container_id == null) {
126             // Sorry we can't do anything without ContainerID
127             throw new ContentDirectoryError.NO_SUCH_OBJECT
128                                         (_("No such object"));
129         }
130     }
131
132     private void parse_didl () throws Error {
133         this.didl_parser.item_available.connect ((didl_item) => {
134             this.didl_item = didl_item;
135         });
136
137         try {
138             this.didl_parser.parse_didl (this.elements);
139         } catch (Error parse_err) {
140             throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
141         }
142
143         if (this.didl_item == null) {
144             var message = _("No items in DIDL-Lite from client: '%s'");
145
146             throw new ItemCreatorError.PARSE (message, this.elements);
147         }
148
149         if (didl_item.id == null || didl_item.id != "") {
150             throw new ContentDirectoryError.BAD_METADATA
151                                         ("@id must be set to \"\" in " +
152                                          "CreateItem");
153         }
154
155         if (didl_item.title == null) {
156             throw new ContentDirectoryError.BAD_METADATA
157                                     ("dc:title must be set in " +
158                                      "CreateItem");
159         }
160
161         // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
162         if ((didl_item.dlna_managed &
163             (OCMFlags.UPLOAD |
164              OCMFlags.CREATE_CONTAINER |
165              OCMFlags.UPLOAD_DESTROYABLE)) != 0) {
166             throw new ContentDirectoryError.BAD_METADATA
167                                         ("Flags that must not be set " +
168                                          "were found in 'dlnaManaged'");
169         }
170
171         if (didl_item.upnp_class == null ||
172             didl_item.upnp_class == "" ||
173             !didl_item.upnp_class.has_prefix ("object.item")) {
174             throw new ContentDirectoryError.BAD_METADATA
175                                         ("Invalid upnp:class given ");
176         }
177
178         if (didl_item.restricted) {
179             throw new ContentDirectoryError.INVALID_ARGS
180                                         ("Cannot create restricted item");
181         }
182     }
183
184     private void generalize_upnp_class (ref string upnp_class) {
185         char *needle = upnp_class.rstr_len (-1, ".");
186         if (needle != null) {
187             *needle = '\0';
188         }
189     }
190
191     /**
192      * Find a container that can create items matching the UPnP class of the
193      * requested item.
194      *
195      * If the item's UPnP class cannot be found, generalize the UPnP class until
196      * we reach object.item according to DLNA guideline 7.3.120.4.
197      *
198      * @returns a container able to create the item or null if no such container
199      *          can be found.
200      */
201     private async MediaObject? find_any_container () throws Error {
202         var root_container = this.content_dir.root_container
203                                         as SearchableContainer;
204
205         if (root_container == null) {
206             return null;
207         }
208
209         var upnp_class = this.didl_item.upnp_class;
210
211         var expression = new RelationalExpression ();
212         expression.op = SearchCriteriaOp.DERIVED_FROM;
213         expression.operand1 = "upnp:createClass";
214
215         while (upnp_class != "object.item") {
216             expression.operand2 = upnp_class;
217
218             uint total_matches;
219             var result = yield root_container.search (expression,
220                                                       0,
221                                                       1,
222                                                       out total_matches,
223                                                       this.cancellable);
224             if (result.size > 0) {
225                 this.didl_item.upnp_class = upnp_class;
226
227                 return result[0];
228             } else {
229                 this.generalize_upnp_class (ref upnp_class);
230             }
231         }
232
233         if (upnp_class == "object.item") {
234             throw new ContentDirectoryError.BAD_METADATA
235                                     ("'%s' UPnP class unsupported",
236                                      this.didl_item.upnp_class);
237         }
238
239         return null;
240     }
241
242     private async WritableContainer fetch_container () throws Error {
243         MediaObject media_object = null;
244
245         if (this.container_id == "DLNA.ORG_AnyContainer") {
246             media_object = yield this.find_any_container ();
247         } else {
248             media_object = yield this.content_dir.root_container.find_object
249                                         (this.container_id, this.cancellable);
250         }
251
252         if (media_object == null || !(media_object is MediaContainer)) {
253             throw new ContentDirectoryError.NO_SUCH_OBJECT
254                                         (_("No such object"));
255         } else if (!(OCMFlags.UPLOAD in media_object.ocm_flags) ||
256                    !(media_object is WritableContainer)) {
257             throw new ContentDirectoryError.RESTRICTED_PARENT
258                                         (_("Object creation in %s not allowed"),
259                                          media_object.id);
260         }
261
262         // FIXME: Check for @restricted=1 missing?
263
264         return media_object as WritableContainer;
265     }
266
267     private void conclude () {
268         /* Retrieve generated string */
269         string didl = this.didl_writer.get_string ();
270
271         /* Set action return arguments */
272         this.action.set ("ObjectID", typeof (string), this.item.id,
273                          "Result", typeof (string), didl);
274
275         this.action.return ();
276         this.completed ();
277     }
278
279     private void handle_error (Error error) {
280         if (error is ContentDirectoryError) {
281             this.action.return_error (error.code, error.message);
282         } else {
283             this.action.return_error (701, error.message);
284         }
285
286         warning (_("Failed to create item under '%s': %s"),
287                  this.container_id,
288                  error.message);
289
290         this.completed ();
291     }
292
293     private string get_generic_mime_type () {
294         if (this.item is ImageItem) {
295             return "image";
296         } else if (this.item is VideoItem) {
297             return "video";
298         } else {
299             return "audio";
300         }
301     }
302
303     private async void create_item_from_didl (WritableContainer container)
304                                                    throws Error {
305         this.item = this.create_item (this.didl_item.id,
306                                       container,
307                                       this.didl_item.title,
308                                       this.didl_item.upnp_class);
309
310         var resources = this.didl_item.get_resources ();
311         if (resources != null && resources.length () > 0) {
312             var resource = resources.nth (0).data;
313             var info = resource.protocol_info;
314
315             if (info != null) {
316                 if (info.dlna_profile != null) {
317                     if (!this.is_profile_valid (info.dlna_profile)) {
318                         throw new ContentDirectoryError.BAD_METADATA
319                                     ("'%s' DLNA profile unsupported",
320                                      info.dlna_profile);
321                     }
322
323                     this.item.dlna_profile = info.dlna_profile;
324                 }
325
326                 if (info.mime_type != null) {
327                     this.item.mime_type = info.mime_type;
328                 }
329             }
330
331             string sanitized_uri;
332             if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
333                 this.item.add_uri (sanitized_uri);
334             }
335
336             if (resource.size >= 0) {
337                 this.item.size = resource.size;
338             }
339         }
340
341         if (this.item.mime_type == null) {
342             this.item.mime_type = this.get_generic_mime_type ();
343         }
344
345         if (this.item.size < 0) {
346             this.item.size = 0;
347         }
348
349         if (this.item.uris.size == 0) {
350             var uri = yield this.create_uri (container, this.item.title);
351             this.item.uris.add (uri);
352             this.item.place_holder = true;
353         } else {
354             var file = File.new_for_uri (this.item.uris[0]);
355             this.item.place_holder = !file.is_native ();
356         }
357
358         this.item.id = this.item.uris[0];
359     }
360
361     private MediaItem create_item (string            id,
362                                    WritableContainer parent,
363                                    string            title,
364                                    string            upnp_class) throws Error {
365         switch (upnp_class) {
366         case ImageItem.UPNP_CLASS:
367             return new ImageItem (id, parent, title);
368         case PhotoItem.UPNP_CLASS:
369             return new PhotoItem (id, parent, title);
370         case VideoItem.UPNP_CLASS:
371             return new VideoItem (id, parent, title);
372         case AudioItem.UPNP_CLASS:
373             return new AudioItem (id, parent, title);
374         case MusicItem.UPNP_CLASS:
375             return new MusicItem (id, parent, title);
376         default:
377             throw new ContentDirectoryError.BAD_METADATA
378                                         ("Creation of item of class '%s' " +
379                                          "not supported.",
380                                          upnp_class);
381         }
382     }
383
384     private bool is_valid_uri (string? uri, out string sanitized_uri) {
385         sanitized_uri = null;
386         if (uri == null || uri == "") {
387             return false;
388         }
389
390         var soup_uri = new Soup.URI (uri);
391
392         if (soup_uri == null || soup_uri.scheme == null) {
393             return false;
394         }
395
396         sanitized_uri = soup_uri.to_string (false);
397
398         return true;
399     }
400
401     private string mangle_title (string title) throws Error {
402         var mangled = title.substring (0, int.min (title.length, 205));
403         mangled = this.title_regex.replace_literal (mangled,
404                                                     -1,
405                                                     0,
406                                                     "_",
407                                                     RegexMatchFlags.NOTEMPTY);
408
409         return mangled;
410     }
411
412     private async string create_uri (WritableContainer container, string title)
413                                     throws Error {
414         var dir = yield container.get_writable (this.cancellable);
415         if (dir == null) {
416             throw new ContentDirectoryError.RESTRICTED_PARENT
417                                         (_("Object creation in %s not allowed"),
418                                          container.id);
419         }
420
421         var file = dir.get_child_for_display_name (this.mangle_title (title));
422
423         var udn = new uchar[50];
424         var id = new uchar[16];
425
426         uuid_generate (id);
427         uuid_unparse (id, udn);
428
429         return file.get_uri () + (string) udn;
430     }
431
432     private async void wait_for_item (WritableContainer container) {
433         debug ("Waiting for new item to appear under container '%s'..",
434                container.id);
435
436         MediaItem item = null;
437
438         while (item == null) {
439             try {
440                 item = (yield container.find_object (this.item.id,
441                                                      this.cancellable))
442                        as MediaItem;
443             } catch (Error error) {
444                 warning ("Error from container '%s' on trying to find newly " +
445                          "added child item '%s' in it",
446                          container.id,
447                          this.item.id);
448             }
449
450             if (item == null) {
451                 var id = container.container_updated.connect ((container) => {
452                     this.wait_for_item.callback ();
453                 });
454
455                 uint timeout = 0;
456                 timeout = Timeout.add_seconds (5, () => {
457                     debug ("Timeout on waiting for 'updated' signal on '%s'.",
458                            container.id);
459                     timeout = 0;
460                     this.wait_for_item.callback ();
461
462                     return false;
463                 });
464
465                 yield;
466
467                 container.disconnect (id);
468
469                 if (timeout != 0) {
470                     Source.remove (timeout);
471                 } else {
472                     break;
473                 }
474             }
475         }
476         debug ("Finished waiting for new item to appear under container '%s'",
477                container.id);
478     }
479
480     private bool is_profile_valid (string profile) {
481         var discoverer = new GUPnP.DLNADiscoverer ((ClockTime) SECOND,
482                                                    true,
483                                                    false);
484
485         var valid = false;
486         foreach (var known_profile in discoverer.list_profiles ()) {
487             if (known_profile.name == profile) {
488                 valid = true;
489
490                 break;
491             }
492         }
493
494         return valid;
495     }
496 }
497