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