summaryrefslogtreecommitdiff
path: root/src/librygel-server/rygel-object-creator.vala
blob: 15f9cae24c0ec61f0022226175d3017707c4afbf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
/*
 * Copyright (C) 2010-2011 Nokia Corporation.
 * Copyright (C) 2012 Intel Corporation.
 *
 * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 *         Jens Georg <jensg@openismus.com>
 *
 * This file is part of Rygel.
 *
 * Rygel is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * Rygel is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

using GUPnP;

/**
 * Dummy implementation of Rygel.MediaContainer to pass on to
 * Rygel.WritableContianer for creation.
 */
private class Rygel.BaseMediaContainer : MediaContainer {
    /**
     * Create a media container with the specified details.
     *
     * @param id See the id property of the #RygelMediaObject class.
     * @param parent The parent container, if any.
     * @param title See the title property of the #RygelMediaObject class.
     * @param child_count The initially-known number of child items.
     */
    public BaseMediaContainer (string          id,
                               MediaContainer? parent,
                               string          title,
                               int             child_count) {
        Object (id : id,
                parent : parent,
                title : title,
                child_count : child_count);
    }

    /**
     * Fetches the list of media objects directly under this container.
     *
     * @param offset zero-based index of the first item to return
     * @param max_count maximum number of objects to return
     * @param sort_criteria sorting order of objects to return
     * @param cancellable optional cancellable for this operation
     *
     * @return A list of media objects.
     */
    public override async MediaObjects? get_children
                                            (uint         offset,
                                             uint         max_count,
                                             string       sort_criteria,
                                             Cancellable? cancellable)
                                            throws Error {
        return null;
    }

    /**
     * Recursively searches this container for a media object with the given ID.
     *
     * @param id ID of the media object to search for
     * @param cancellable optional cancellable for this operation
     *
     * @return the found media object.
     */
    public override async MediaObject? find_object (string       id,
                                                    Cancellable? cancellable)
                                                    throws Error {
        return null;
    }
}



/**
 * CreateObject action implementation.
 */
internal class Rygel.ObjectCreator: GLib.Object, Rygel.StateMachine {
    private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");

    private const string INVALID_CHARS = "/?<>\\:*|\"";

    // In arguments
    private string container_id;
    private string elements;

    private DIDLLiteObject didl_object;
    private MediaObject object;

    private ContentDirectory content_dir;
    private ServiceAction action;
    private Serializer serializer;
    private DIDLLiteParser didl_parser;
    private Regex title_regex;

    public Cancellable cancellable { get; set; }

    public ObjectCreator (ContentDirectory    content_dir,
                          owned ServiceAction action) {
        this.content_dir = content_dir;
        this.cancellable = content_dir.cancellable;
        this.action = (owned) action;
        this.serializer = new Serializer (SerializerType.GENERIC_DIDL);
        this.didl_parser = new DIDLLiteParser ();
        try {
            var pattern = "[" + Regex.escape_string (INVALID_CHARS) + "]";
            this.title_regex = new Regex (pattern,
                                          RegexCompileFlags.OPTIMIZE,
                                          RegexMatchFlags.NOTEMPTY);
        } catch (Error error) { assert_not_reached (); }
    }

    public async void run () {
        try {
            this.parse_args ();
            this.parse_didl ();

            var container = yield this.fetch_container ();

            /* Verify the create class. Note that we always assume
             * createClass@includeDerived to be false.
             *
             * DLNA.ORG_AnyContainer is a special case. We are allowed to
             * modify the UPnP class to something we support and
             * fetch_container took care of this already.
             */
            if (!container.can_create (this.didl_object.upnp_class) &&
                this.container_id != MediaContainer.ANY) {
                throw new ContentDirectoryError.BAD_METADATA
                                        ("Creating of objects with class %s " +
                                         "is not supported in %s",
                                         this.didl_object.upnp_class,
                                         container.id);
            }

            if (this.didl_object is DIDLLiteContainer &&
                !this.validate_create_class (container)) {
                throw new ContentDirectoryError.BAD_METADATA
                                   (_("upnp:createClass value not supported"));
            }

            yield this.create_object_from_didl (container);
            if (this.object is MediaFileItem) {
                yield container.add_item ((MediaFileItem) this.object,
                                          this.cancellable);
            } else {
                yield container.add_container ((MediaContainer) this.object,
                                               this.cancellable);
            }

            yield this.wait_for_object (container);

            this.object.serialize (serializer, this.content_dir.http_server);

            // Conclude the successful action
            this.conclude ();

            var item = this.object as MediaFileItem;

            if (this.container_id == MediaContainer.ANY &&
                (item != null) &&
                item.place_holder) {
                var queue = ObjectRemovalQueue.get_default ();

                queue.queue (this.object, this.cancellable);
            }
        } catch (Error err) {
            this.handle_error (err);
        }
    }

    /**
     * Check the supplied input parameters.
     */
    private void parse_args () throws Error {
        /* Start by parsing the 'in' arguments */
        this.action.get ("ContainerID", typeof (string), out this.container_id,
                         "Elements", typeof (string), out this.elements);

        if (this.elements == null) {
            throw new ContentDirectoryError.BAD_METADATA
                                        (_("“Elements” argument missing."));
        } else if (comment_pattern.match_string (this.elements)) {
            throw new ContentDirectoryError.BAD_METADATA
                                        (_("Comments not allowed in XML"));
        }

        if (this.container_id == null) {
            // Sorry we can't do anything without ContainerID
            throw new ContentDirectoryError.INVALID_ARGS
                                        (_("Missing ContainerID argument"));
        }
    }

    /**
     * Parse the given DIDL-Lite snippet.
     *
     * Parses the DIDL-Lite and performs checking of the passed meta-data
     * according to UPnP and DLNA guidelines.
     */
    private void parse_didl () throws Error {
        // FIXME: This will take the last object in the DIDL-Lite, maybe we
        // should limit it to one somehow.
        this.didl_parser.object_available.connect ((didl_object) => {
            this.didl_object = didl_object;
        });

        try {
            this.didl_parser.parse_didl (this.elements);
        } catch (Error parse_err) {
            throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
        }

        if (this.didl_object == null) {
            var message = _("No objects in DIDL-Lite from client: “%s”");

            throw new ContentDirectoryError.BAD_METADATA
                                        (message, this.elements);
        }

        if (didl_object.id == null || didl_object.id != "") {
            var msg = _("@id must be set to \"\" in CreateObject call");
            throw new ContentDirectoryError.BAD_METADATA (msg);
        }

        if (didl_object.title == null) {
            var msg = _("dc:title must not be empty in CreateObject call");
            throw new ContentDirectoryError.BAD_METADATA (msg);
        }

        // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
        // These flags must not be set on items.
        if (didl_object is DIDLLiteItem &&
            ((didl_object.dlna_managed &
             (OCMFlags.UPLOAD |
              OCMFlags.CREATE_CONTAINER |
              OCMFlags.UPLOAD_DESTROYABLE)) != 0)) {
            var msg =  _("Flags that must not be set were found in “dlnaManaged”");
            throw new ContentDirectoryError.BAD_METADATA (msg);
        }

        if (didl_object.upnp_class == null ||
            didl_object.upnp_class == "" ||
            !didl_object.upnp_class.has_prefix ("object")) {
            throw new ContentDirectoryError.BAD_METADATA
                                        (_("Invalid upnp:class given in CreateObject"));
        }

        if (!didl_object.is_restricted_set ()) {
            var msg = _("Object is missing the @restricted attribute");
            throw new ContentDirectoryError.BAD_METADATA (msg);
        }


        if (didl_object.restricted) {
            throw new ContentDirectoryError.BAD_METADATA
                                        (_("Cannot create restricted item"));
        }

        // Handle DIDL_S items...
        if (this.didl_object.upnp_class == "object.item") {
            var resources = this.didl_object.get_resources ();
            if (resources != null &&
                resources.data.protocol_info.dlna_profile == "DIDL_S") {
                this.didl_object.upnp_class = PlaylistItem.UPNP_CLASS;
            }
        }
    }

    /**
     * Modify the give UPnP class to be a more general one.
     *
     * Used to simplify the search for a valid container in the
     * DLNA.ORG_AnyContainer use-case.
     * Example: object.item.videoItem.videoBroadcast → object.item.videoItem
     *
     * @param upnp_class the current UPnP class which will be modified in-place.
     */
    private void generalize_upnp_class (ref string upnp_class) {
        char *needle = upnp_class.rstr_len (-1, ".");
        if (needle != null) {
            *needle = '\0';
        }
    }

    private async SearchExpression build_create_class_expression
                                        (SearchExpression expression) {
        // Take create-classes into account
        if (!(this.didl_object is DIDLLiteContainer)) {
            return expression;
        }

        var didl_container = this.didl_object as DIDLLiteContainer;
        var create_classes = didl_container.get_create_classes ();
        if (create_classes == null) {
            return expression;
        }

        var builder = new StringBuilder ("(");
        foreach (var create_class in create_classes) {
            builder.append_printf ("(upnp:createClass derivedfrom \"%s\") AND",
                                   create_class);
        }

        // remove dangeling AND
        builder.truncate (builder.len - 3);
        builder.append (")");

        try {
            var parser = new Rygel.SearchCriteriaParser (builder.str);
            yield parser.run ();

            var rel = new LogicalExpression ();
            rel.operand1 = expression;
            rel.op = LogicalOperator.AND;
            rel.operand2 = parser.expression;

            return rel;
        } catch (Error error) {
            assert_not_reached ();
        }
    }

    /**
     * Find a container that can create items matching the UPnP class of the
     * requested item.
     *
     * If the item's UPnP class cannot be found, generalize the UPnP class until
     * we reach object.item according to DLNA guideline 7.3.120.4.
     *
     * @returns a container able to create the item or null if no such container
     *          can be found.
     */
    private async MediaObject? find_any_container () throws Error {
        var root_container = this.content_dir.root_container
                                        as SearchableContainer;

        if (root_container == null) {
            return null;
        }

        var upnp_class = this.didl_object.upnp_class;

        var expression = new RelationalExpression ();
        expression.op = SearchCriteriaOp.DERIVED_FROM;
        expression.operand1 = "upnp:createClass";

        // Add container's create classes to the search expression if there
        // are some
        var search_expression = yield this.build_create_class_expression
                                        (expression);

        while (upnp_class != "object") {
            expression.operand2 = upnp_class;

            uint total_matches;
            var result = yield root_container.search (search_expression,
                                                      0,
                                                      1,
                                                      root_container.sort_criteria,
                                                      this.cancellable,
                                                      out total_matches);
            if (result.size > 0) {
                this.didl_object.upnp_class = upnp_class;

                return result[0];
            } else {
                this.generalize_upnp_class (ref upnp_class);
            }
        }

        if (upnp_class == "object") {
            throw new ContentDirectoryError.BAD_METADATA
                                    (_("UPnP class “%s” not supported"),
                                     this.didl_object.upnp_class);
        }

        return null;
    }

    /**
     * Get the container to create the item in.
     *
     * This will either try to fetch the container supplied by the caller or
     * search for a container if the caller supplied the "DLNA.ORG_AnyContainer"
     * id.
     *
     * @return an instance of WritableContainer matching the criteria
     * @throws ContentDirectoryError for various problems
     */
    private async WritableContainer fetch_container () throws Error {
        MediaObject media_object = null;

        if (this.container_id == MediaContainer.ANY) {
            media_object = yield this.find_any_container ();
        } else {
            media_object = yield this.content_dir.root_container.find_object
                                        (this.container_id, this.cancellable);
        }

        if (media_object == null || !(media_object is MediaContainer)) {
            throw new ContentDirectoryError.NO_SUCH_CONTAINER
                                        (_("No such container"));
        }

        if (!(media_object is WritableContainer)) {
            throw new ContentDirectoryError.RESTRICTED_PARENT
                                        (_("Object creation in %s not allowed"),
                                         media_object.id);
        }

        // If the object to be created is an item, ocm_flags must contain
        // OCMFlags.UPLOAD, it it's a container, ocm_flags must contain
        // OCMFlags.CREATE_CONTAINER
        if (!((this.didl_object is DIDLLiteItem &&
            (OCMFlags.UPLOAD in media_object.ocm_flags)) ||
           (this.didl_object is DIDLLiteContainer &&
            (OCMFlags.CREATE_CONTAINER in media_object.ocm_flags)))) {
            throw new ContentDirectoryError.RESTRICTED_PARENT
                                        (_("Object creation in %s not allowed"),
                                         media_object.id);
        }

        // FIXME: Check for @restricted=1 missing?

        return media_object as WritableContainer;
    }

    private void conclude () {
        /* Retrieve generated string */
        string didl = this.serializer.get_string ();

        /* Set action return arguments */
        this.action.set ("ObjectID", typeof (string), this.object.id,
                         "Result", typeof (string), didl);

        this.action.return_success ();
        this.completed ();
    }

    private bool validate_create_class (WritableContainer container) {
        var didl_cont = this.didl_object as DIDLLiteContainer;
        var create_classes = didl_cont.get_create_classes ();

        if (create_classes == null) {
            return true;
        }

        foreach (var create_class in create_classes) {
            if (!container.can_create (create_class)) {
                return false;
            }
        }

        return true;
    }

    private void handle_error (Error error) {
        if (error is ContentDirectoryError) {
            this.action.return_error (error.code, error.message);
        } else {
            this.action.return_error (701, error.message);
        }

        warning (_("Failed to create item under “%s”: %s"),
                 this.container_id,
                 error.message);

        this.completed ();
    }

    private string get_generic_mime_type () {
        if (!(this.object is MediaFileItem)) {
            return "";
        }

        var item = this.object as MediaFileItem;

        if (item is ImageItem) {
            return "image";
        } else if (item is VideoItem) {
            return "video";
        } else {
            return "audio";
        }
    }

    /**
     * Transfer information passed by caller to a MediaObject.
     *
     * WritableContainer works on MediaObject so we transfer the supplied data
     * to one. Additionally some checks are performed (e.g. whether the DLNA
     * profile is supported or not) or sanitize the supplied title for use as
     * part of the on-disk filename.
     *
     * This function fills ObjectCreator.object.
     */
    private async void create_object_from_didl (WritableContainer container)
                                                throws Error {
        this.object = this.create_object (this.didl_object.id,
                                          container,
                                          this.didl_object.title,
                                          this.didl_object.upnp_class);

        this.object.apply_didl_lite (this.didl_object);

        if (this.object is MediaItem) {
            this.extract_item_parameters ();
        }

        // extract_item_parameters could not find an uri
        var item = this.object as MediaFileItem;

        if (this.object.get_uris ().is_empty) {
            var uri = yield this.create_uri (container, this.object.title);
            this.object.add_uri (uri);
            if (item != null) {
                item.place_holder = true;
            }
        } else {
            if (item != null) {
                var file = File.new_for_uri (this.object.get_primary_uri ());
                item.place_holder = !file.is_native ();
            }
        }

        this.object.id = this.object.get_primary_uri ();

        this.parse_and_verify_didl_date ();
    }

    private void extract_item_parameters () throws Error {
        var item = this.object as MediaFileItem;

        foreach (var resource in this.didl_object.get_resources ()) {
            var info = resource.protocol_info;

            if (info != null) {
                if (info.dlna_profile != null) {
                    if (!this.is_profile_valid (info.dlna_profile)) {
                        var msg = _("DLNA profile “%s” not supported");
                        throw new ContentDirectoryError.BAD_METADATA
                                    (msg,
                                     info.dlna_profile);
                    }

                    item.dlna_profile = info.dlna_profile;
                }

                if (info.mime_type != null) {
                    item.mime_type = info.mime_type;
                }
            }

            string sanitized_uri = null;
            if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
                item.add_uri (sanitized_uri);
            }

            if (resource.size >= 0) {
                item.size = resource.size;
            }
        }

        if (item.mime_type == null) {
            item.mime_type = this.get_generic_mime_type ();
        }

        if (item.size < 0) {
            item.size = 0;
        }
    }

    private void parse_and_verify_didl_date () throws Error {
        var didl_item = this.didl_object as DIDLLiteItem;
        if (didl_item == null) {
            return;
        }

        var item = this.object as MediaFileItem;
        if (item == null) {
            return;
        }

        if (didl_item.date == null) {
            return;
        }

        var parsed_date = new GLib.DateTime.from_iso8601 (didl_item.date, null);
        if (parsed_date != null) {
            item.date = GUPnP.format_date_time_for_didl_lite (parsed_date);

            return;
        }

        int year = 0, month = 0, day = 0;

        if (didl_item.date.scanf ("%4d-%02d-%02d",
                                  out year,
                                  out month,
                                  out day) != 3) {
            throw new ContentDirectoryError.BAD_METADATA
                                    (_("Invalid date format: %s"),
                                     didl_item.date);
        }

        var date = GLib.Date ();
        date.set_dmy ((DateDay) day, (DateMonth) month, (DateYear) year);

        if (!date.valid ()) {
            throw new ContentDirectoryError.BAD_METADATA
                                    (_("Invalid date: %s"),
                                     didl_item.date);
        }

        item.date = didl_item.date + "T00:00:00";
    }

    private MediaObject create_object (string            id,
                                       WritableContainer parent,
                                       string            title,
                                       string            upnp_class)
                                       throws Error {
        switch (upnp_class) {
        case ImageItem.UPNP_CLASS:
            return new ImageItem (id, parent, title);
        case PhotoItem.UPNP_CLASS:
            return new PhotoItem (id, parent, title);
        case VideoItem.UPNP_CLASS:
            return new VideoItem (id, parent, title);
        case AudioItem.UPNP_CLASS:
            return new AudioItem (id, parent, title);
        case MusicItem.UPNP_CLASS:
            return new MusicItem (id, parent, title);
        case PlaylistItem.UPNP_CLASS:
            return new PlaylistItem (id, parent, title);
        case MediaContainer.UPNP_CLASS:
        case MediaContainer.STORAGE_FOLDER:
            return new BaseMediaContainer (id, parent, title, 0);
        case MediaContainer.PLAYLIST:
            var container = new BaseMediaContainer (id, parent, title, 0);
            container.upnp_class = upnp_class;
            return container;
        default:
            var msg = _("Cannot create object of class “%s”: Not supported");
            throw new ContentDirectoryError.BAD_METADATA (msg, upnp_class);
        }
    }

    /**
     * Simple check for the validity of an URI.
     *
     * Check is done by parsing the URI with soup. Additionaly a cleaned-up
     * version of the URI is returned in sanitized_uri.
     *
     * @param uri the input URI
     * @param sanitized_uri containes a sanitized version of the URI on return
     * @returns true if the URI is valid, false otherwise.
     */
    private bool is_valid_uri (string? uri, out string sanitized_uri) {
        sanitized_uri = null;
        if (uri == null || uri == "") {
            return false;
        }

        try {
            var parsed_uri = GLib.Uri.parse (uri, GLib.UriFlags.NONE);
            sanitized_uri = parsed_uri.to_string();
            return true;
        } catch (Error err) {
            return false;
        }
    }

    /**
     * Transform the title to be usable on legacy file-systems such as FAT32.
     *
     * The function trims down the title to 205 chars (leaving room for an UUID)
     * and replaces all special characters.
     *
     * @param title of the the media item
     * @return the cleaned and shortened title
     */
    private string mangle_title (string title) throws Error {
        var mangled = title.substring (0, int.min (title.length, 205));
        mangled = this.title_regex.replace_literal (mangled,
                                                    -1,
                                                    0,
                                                    "_",
                                                    RegexMatchFlags.NOTEMPTY);

        return GUPnP.get_uuid () + "-" + mangled;
    }

    /**
     * Create an URI from the item's title.
     *
     * Create an unique URI from the supplied title by cleaning it from
     * unwanted characters, shortening it and adding an UUID.
     *
     * @param container to create the item in
     * @param title of the item to base the name on
     * @returns an URI for the newly created item
     */
    private async string create_uri (WritableContainer container, string title)
                                    throws Error {
        var dir = yield container.get_writable (this.cancellable);
        if (dir == null) {
            throw new ContentDirectoryError.RESTRICTED_PARENT
                                        (_("Object creation in %s not allowed"),
                                         container.id);
        }

        var file = dir.get_child_for_display_name (this.mangle_title (title));

        return file.get_uri ();
    }

    /**
     * Wait for the new object
     *
     * When creating an object in the back-end via WritableContainer.add_item
     * or WritableContainer.add_container there might be a delay between the
     * creation and the back-end having the newly created item available. This
     * function waits for the item to become available by hooking into the
     * container_updated signal. The maximum time to wait is 5 seconds.
     *
     * @param container to watch
     */
    private async void wait_for_object (WritableContainer container) {
        debug ("Waiting for new object to appear under container '%s'…",
               container.id);

        MediaObject object = null;

        while (object == null) {
            try {
                object = yield container.find_object (this.object.id,
                                                      this.cancellable);
            } catch (Error error) {
                var msg = _("Error from container “%s” on trying to find the newly added child object “%s” in it: %s");
                warning (msg, container.id, this.object.id, error.message);
            }

            if (object == null) {
                var id = container.container_updated.connect ((container) => {
                    this.wait_for_object.callback ();
                });

                uint timeout = 0;
                timeout = Timeout.add_seconds (5, () => {
                    debug ("Timeout on waiting for 'updated' signal on '%s'.",
                           container.id);
                    timeout = 0;
                    this.wait_for_object.callback ();

                    return false;
                });

                yield;

                container.disconnect (id);

                if (timeout != 0) {
                    Source.remove (timeout);
                } else {
                    break;
                }
            }
        }
        debug ("Finished waiting for new object to appear under container '%s'",
               container.id);

        this.object = object;
    }

    /**
     * Check if the profile is supported.
     *
     * The check is performed against the MediaEngine's database explicitly excluding
     * the transcoders.
     *
     * @param profile to check
     * @returns true if the profile is supported, false otherwise.
     */
    private bool is_profile_valid (string profile) {
        unowned GLib.List<DLNAProfile> profiles, result;

        var plugin = this.content_dir.root_device.resource_factory as MediaServerPlugin;
        profiles = plugin.upload_profiles;
        var p = new DLNAProfile (profile, "");

        result = profiles.find_custom (p, DLNAProfile.compare_by_name);

        return result != null;
    }
}