~ubuntu-branches/ubuntu/raring/rygel/raring

« back to all changes in this revision

Viewing changes to src/librygel-server/rygel-item-creator.vala

  • Committer: Package Import Robot
  • Author(s): Andreas Henriksson
  • Date: 2012-09-26 22:34:15 UTC
  • mfrom: (11.1.19 experimental)
  • Revision ID: package-import@ubuntu.com-20120926223415-9day2s783n9td4e8
Tags: 0.16.0-1
ImportedĀ UpstreamĀ versionĀ 0.16.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
    /**
 
109
     * Check the supplied input parameters.
 
110
     */
 
111
    private void parse_args () throws Error {
 
112
        /* Start by parsing the 'in' arguments */
 
113
        this.action.get ("ContainerID", typeof (string), out this.container_id,
 
114
                         "Elements", typeof (string), out this.elements);
 
115
 
 
116
        if (this.elements == null) {
 
117
            throw new ContentDirectoryError.BAD_METADATA
 
118
                                        (_("'Elements' argument missing."));
 
119
        } else if (comment_pattern.match_string (this.elements)) {
 
120
            throw new ContentDirectoryError.BAD_METADATA
 
121
                                        (_("Comments not allowed in XML"));
 
122
        }
 
123
 
 
124
        if (this.container_id == null) {
 
125
            // Sorry we can't do anything without ContainerID
 
126
            throw new ContentDirectoryError.NO_SUCH_OBJECT
 
127
                                        (_("No such object"));
 
128
        }
 
129
    }
 
130
 
 
131
    /**
 
132
     * Parse the given DIDL-Lite snippet.
 
133
     *
 
134
     * Parses the DIDL-Lite and performs checking of the passed meta-data
 
135
     * according to UPnP and DLNA guidelines.
 
136
     */
 
137
    private void parse_didl () throws Error {
 
138
        this.didl_parser.item_available.connect ((didl_item) => {
 
139
            this.didl_item = didl_item;
 
140
        });
 
141
 
 
142
        try {
 
143
            this.didl_parser.parse_didl (this.elements);
 
144
        } catch (Error parse_err) {
 
145
            throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
 
146
        }
 
147
 
 
148
        if (this.didl_item == null) {
 
149
            var message = _("No items in DIDL-Lite from client: '%s'");
 
150
 
 
151
            throw new ContentDirectoryError.BAD_METADATA
 
152
                                        (message, this.elements);
 
153
        }
 
154
 
 
155
        if (didl_item.id == null || didl_item.id != "") {
 
156
            throw new ContentDirectoryError.BAD_METADATA
 
157
                                        ("@id must be set to \"\" in " +
 
158
                                         "CreateItem");
 
159
        }
 
160
 
 
161
        if (didl_item.title == null) {
 
162
            throw new ContentDirectoryError.BAD_METADATA
 
163
                                    ("dc:title must be set in " +
 
164
                                     "CreateItem");
 
165
        }
 
166
 
 
167
        // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
 
168
        if ((didl_item.dlna_managed &
 
169
            (OCMFlags.UPLOAD |
 
170
             OCMFlags.CREATE_CONTAINER |
 
171
             OCMFlags.UPLOAD_DESTROYABLE)) != 0) {
 
172
            throw new ContentDirectoryError.BAD_METADATA
 
173
                                        ("Flags that must not be set " +
 
174
                                         "were found in 'dlnaManaged'");
 
175
        }
 
176
 
 
177
        if (didl_item.upnp_class == null ||
 
178
            didl_item.upnp_class == "" ||
 
179
            !didl_item.upnp_class.has_prefix ("object.item")) {
 
180
            throw new ContentDirectoryError.BAD_METADATA
 
181
                                        ("Invalid upnp:class given ");
 
182
        }
 
183
 
 
184
        if (didl_item.restricted) {
 
185
            throw new ContentDirectoryError.INVALID_ARGS
 
186
                                        ("Cannot create restricted item");
 
187
        }
 
188
    }
 
189
 
 
190
    /**
 
191
     * Modify the give UPnP class to be a more general one.
 
192
     *
 
193
     * Used to simplify the search for a valid container in the
 
194
     * DLNA.ORG_AnyContainer use-case.
 
195
     * Example: object.item.videoItem.videoBroadcast ā†’ object.item.videoItem
 
196
     *
 
197
     * @param upnp_class the current UPnP class which will be modified in-place.
 
198
     */
 
199
    private void generalize_upnp_class (ref string upnp_class) {
 
200
        char *needle = upnp_class.rstr_len (-1, ".");
 
201
        if (needle != null) {
 
202
            *needle = '\0';
 
203
        }
 
204
    }
 
205
 
 
206
    /**
 
207
     * Find a container that can create items matching the UPnP class of the
 
208
     * requested item.
 
209
     *
 
210
     * If the item's UPnP class cannot be found, generalize the UPnP class until
 
211
     * we reach object.item according to DLNA guideline 7.3.120.4.
 
212
     *
 
213
     * @returns a container able to create the item or null if no such container
 
214
     *          can be found.
 
215
     */
 
216
    private async MediaObject? find_any_container () throws Error {
 
217
        var root_container = this.content_dir.root_container
 
218
                                        as SearchableContainer;
 
219
 
 
220
        if (root_container == null) {
 
221
            return null;
 
222
        }
 
223
 
 
224
        var upnp_class = this.didl_item.upnp_class;
 
225
 
 
226
        var expression = new RelationalExpression ();
 
227
        expression.op = SearchCriteriaOp.DERIVED_FROM;
 
228
        expression.operand1 = "upnp:createClass";
 
229
 
 
230
        while (upnp_class != "object.item") {
 
231
            expression.operand2 = upnp_class;
 
232
 
 
233
            uint total_matches;
 
234
            var result = yield root_container.search (expression,
 
235
                                                      0,
 
236
                                                      1,
 
237
                                                      out total_matches,
 
238
                                                      root_container.sort_criteria,
 
239
                                                      this.cancellable);
 
240
            if (result.size > 0) {
 
241
                this.didl_item.upnp_class = upnp_class;
 
242
 
 
243
                return result[0];
 
244
            } else {
 
245
                this.generalize_upnp_class (ref upnp_class);
 
246
            }
 
247
        }
 
248
 
 
249
        if (upnp_class == "object.item") {
 
250
            throw new ContentDirectoryError.BAD_METADATA
 
251
                                    ("'%s' UPnP class unsupported",
 
252
                                     this.didl_item.upnp_class);
 
253
        }
 
254
 
 
255
        return null;
 
256
    }
 
257
 
 
258
    /**
 
259
     * Get the container to create the item in.
 
260
     *
 
261
     * This will either try to fetch the container supplied by the caller or
 
262
     * search for a container if the caller supplied the "DLNA.ORG_AnyContainer"
 
263
     * id.
 
264
     *
 
265
     * @return a instance of WritableContainer matching the criteria
 
266
     * @throws ContentDirectoryError for various problems
 
267
     */
 
268
    private async WritableContainer fetch_container () throws Error {
 
269
        MediaObject media_object = null;
 
270
 
 
271
        if (this.container_id == "DLNA.ORG_AnyContainer") {
 
272
            media_object = yield this.find_any_container ();
 
273
        } else {
 
274
            media_object = yield this.content_dir.root_container.find_object
 
275
                                        (this.container_id, this.cancellable);
 
276
        }
 
277
 
 
278
        if (media_object == null || !(media_object is MediaContainer)) {
 
279
            throw new ContentDirectoryError.NO_SUCH_OBJECT
 
280
                                        (_("No such object"));
 
281
        } else if (!(OCMFlags.UPLOAD in media_object.ocm_flags) ||
 
282
                   !(media_object is WritableContainer)) {
 
283
            throw new ContentDirectoryError.RESTRICTED_PARENT
 
284
                                        (_("Object creation in %s not allowed"),
 
285
                                         media_object.id);
 
286
        }
 
287
 
 
288
        // FIXME: Check for @restricted=1 missing?
 
289
 
 
290
        return media_object as WritableContainer;
 
291
    }
 
292
 
 
293
    private void conclude () {
 
294
        /* Retrieve generated string */
 
295
        string didl = this.didl_writer.get_string ();
 
296
 
 
297
        /* Set action return arguments */
 
298
        this.action.set ("ObjectID", typeof (string), this.item.id,
 
299
                         "Result", typeof (string), didl);
 
300
 
 
301
        this.action.return ();
 
302
        this.completed ();
 
303
    }
 
304
 
 
305
    private void handle_error (Error error) {
 
306
        if (error is ContentDirectoryError) {
 
307
            this.action.return_error (error.code, error.message);
 
308
        } else {
 
309
            this.action.return_error (701, error.message);
 
310
        }
 
311
 
 
312
        warning (_("Failed to create item under '%s': %s"),
 
313
                 this.container_id,
 
314
                 error.message);
 
315
 
 
316
        this.completed ();
 
317
    }
 
318
 
 
319
    private string get_generic_mime_type () {
 
320
        if (this.item is ImageItem) {
 
321
            return "image";
 
322
        } else if (this.item is VideoItem) {
 
323
            return "video";
 
324
        } else {
 
325
            return "audio";
 
326
        }
 
327
    }
 
328
 
 
329
    /**
 
330
     * Transfer information passed by caller to a MediaItem.
 
331
     *
 
332
     * WritableContainer works on MediaItem so we transfer the supplied data to
 
333
     * one. Additionally some checks are performed (e.g. whether the DLNA
 
334
     * profile is supported or not) or sanitize the supplied title for use as
 
335
     * part of the on-disk filename.
 
336
     *
 
337
     * This function fills ItemCreator.item.
 
338
     */
 
339
    private async void create_item_from_didl (WritableContainer container)
 
340
                                                   throws Error {
 
341
        this.item = this.create_item (this.didl_item.id,
 
342
                                      container,
 
343
                                      this.didl_item.title,
 
344
                                      this.didl_item.upnp_class);
 
345
 
 
346
        var resources = this.didl_item.get_resources ();
 
347
        if (resources != null && resources.length () > 0) {
 
348
            var resource = resources.nth (0).data;
 
349
            var info = resource.protocol_info;
 
350
 
 
351
            if (info != null) {
 
352
                if (info.dlna_profile != null) {
 
353
                    if (!this.is_profile_valid (info.dlna_profile)) {
 
354
                        throw new ContentDirectoryError.BAD_METADATA
 
355
                                    ("'%s' DLNA profile unsupported",
 
356
                                     info.dlna_profile);
 
357
                    }
 
358
 
 
359
                    this.item.dlna_profile = info.dlna_profile;
 
360
                }
 
361
 
 
362
                if (info.mime_type != null) {
 
363
                    this.item.mime_type = info.mime_type;
 
364
                }
 
365
            }
 
366
 
 
367
            string sanitized_uri;
 
368
            if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
 
369
                this.item.add_uri (sanitized_uri);
 
370
            }
 
371
 
 
372
            if (resource.size >= 0) {
 
373
                this.item.size = resource.size;
 
374
            }
 
375
        }
 
376
 
 
377
        if (this.item.mime_type == null) {
 
378
            this.item.mime_type = this.get_generic_mime_type ();
 
379
        }
 
380
 
 
381
        if (this.item.size < 0) {
 
382
            this.item.size = 0;
 
383
        }
 
384
 
 
385
        if (this.item.uris.size == 0) {
 
386
            var uri = yield this.create_uri (container, this.item.title);
 
387
            this.item.uris.add (uri);
 
388
            this.item.place_holder = true;
 
389
        } else {
 
390
            var file = File.new_for_uri (this.item.uris[0]);
 
391
            this.item.place_holder = !file.is_native ();
 
392
        }
 
393
 
 
394
        this.item.id = this.item.uris[0];
 
395
 
 
396
        this.parse_and_verify_didl_date ();
 
397
    }
 
398
 
 
399
    private void parse_and_verify_didl_date () throws Error {
 
400
        if (this.didl_item.date == null) {
 
401
            return;
 
402
        }
 
403
 
 
404
        var parsed_date = new Soup.Date.from_string (this.didl_item.date);
 
405
        if (parsed_date != null) {
 
406
            this.item.date = parsed_date.to_string (Soup.DateFormat.ISO8601);
 
407
 
 
408
            return;
 
409
        }
 
410
 
 
411
        int year = 0, month = 0, day = 0;
 
412
 
 
413
        if (this.didl_item.date.scanf ("%4d-%02d-%02d",
 
414
                                       out year,
 
415
                                       out month,
 
416
                                       out day) != 3) {
 
417
            throw new ContentDirectoryError.BAD_METADATA
 
418
                                    ("Invalid date format: %s",
 
419
                                     this.didl_item.date);
 
420
        }
 
421
 
 
422
        var date = GLib.Date ();
 
423
        date.set_dmy ((DateDay) day, (DateMonth) month, (DateYear) year);
 
424
 
 
425
        if (!date.valid ()) {
 
426
            throw new ContentDirectoryError.BAD_METADATA
 
427
                                    ("Invalid date: %s",
 
428
                                     this.didl_item.date);
 
429
        }
 
430
 
 
431
        this.item.date = this.didl_item.date + "T00:00:00";
 
432
    }
 
433
 
 
434
    private MediaItem create_item (string            id,
 
435
                                   WritableContainer parent,
 
436
                                   string            title,
 
437
                                   string            upnp_class) throws Error {
 
438
        switch (upnp_class) {
 
439
        case ImageItem.UPNP_CLASS:
 
440
            return new ImageItem (id, parent, title);
 
441
        case PhotoItem.UPNP_CLASS:
 
442
            return new PhotoItem (id, parent, title);
 
443
        case VideoItem.UPNP_CLASS:
 
444
            return new VideoItem (id, parent, title);
 
445
        case AudioItem.UPNP_CLASS:
 
446
            return new AudioItem (id, parent, title);
 
447
        case MusicItem.UPNP_CLASS:
 
448
            return new MusicItem (id, parent, title);
 
449
        default:
 
450
            throw new ContentDirectoryError.BAD_METADATA
 
451
                                        ("Creation of item of class '%s' " +
 
452
                                         "not supported.",
 
453
                                         upnp_class);
 
454
        }
 
455
    }
 
456
 
 
457
    /**
 
458
     * Simple check for the validity of an URI.
 
459
     *
 
460
     * Check is done by parsing the URI with soup. Additionaly a cleaned-up
 
461
     * version of the URI is returned in sanitized_uri.
 
462
     *
 
463
     * @param uri the input URI
 
464
     * @param sanitized_uri containes a sanitized version of the URI on return
 
465
     * @returns true if the URI is valid, false otherwise.
 
466
     */
 
467
    private bool is_valid_uri (string? uri, out string sanitized_uri) {
 
468
        sanitized_uri = null;
 
469
        if (uri == null || uri == "") {
 
470
            return false;
 
471
        }
 
472
 
 
473
        var soup_uri = new Soup.URI (uri);
 
474
 
 
475
        if (soup_uri == null || soup_uri.scheme == null) {
 
476
            return false;
 
477
        }
 
478
 
 
479
        sanitized_uri = soup_uri.to_string (false);
 
480
 
 
481
        return true;
 
482
    }
 
483
 
 
484
    /**
 
485
     * Transform the title to be usable on legacy file-systems such as FAT32.
 
486
     *
 
487
     * The function trims down the title to 205 chars (leaving room for an UUID)
 
488
     * and replaces all special characters.
 
489
     *
 
490
     * @param title of the the media item
 
491
     * @return the cleaned and shortened title
 
492
     */
 
493
    private string mangle_title (string title) throws Error {
 
494
        var mangled = title.substring (0, int.min (title.length, 205));
 
495
        mangled = this.title_regex.replace_literal (mangled,
 
496
                                                    -1,
 
497
                                                    0,
 
498
                                                    "_",
 
499
                                                    RegexMatchFlags.NOTEMPTY);
 
500
 
 
501
        var udn = new uchar[50];
 
502
        var id = new uchar[16];
 
503
 
 
504
        UUID.generate (id);
 
505
        UUID.unparse (id, udn);
 
506
 
 
507
        return (string) udn + "-" + mangled;
 
508
    }
 
509
 
 
510
    /**
 
511
     * Create an URI from the item's title.
 
512
     *
 
513
     * Create an unique URI from the supplied title by cleaning it from
 
514
     * unwanted characters, shortening it and adding an UUID.
 
515
     *
 
516
     * @param container to create the item in
 
517
     * @param title of the item to base the name on
 
518
     * @returns an URI for the newly created item
 
519
     */
 
520
    private async string create_uri (WritableContainer container, string title)
 
521
                                    throws Error {
 
522
        var dir = yield container.get_writable (this.cancellable);
 
523
        if (dir == null) {
 
524
            throw new ContentDirectoryError.RESTRICTED_PARENT
 
525
                                        (_("Object creation in %s not allowed"),
 
526
                                         container.id);
 
527
        }
 
528
 
 
529
        var file = dir.get_child_for_display_name (this.mangle_title (title));
 
530
 
 
531
        return file.get_uri ();
 
532
    }
 
533
 
 
534
    /**
 
535
     * Wait for the new item
 
536
     *
 
537
     * When creating an item in the back-end via WritableContainer.add_item
 
538
     * there might be a delay between the creation and the back-end having the
 
539
     * newly created item available. This function waits for the item to become
 
540
     * available by hooking into the container_updated signal. The maximum time
 
541
     * to wait is 5 seconds.
 
542
     *
 
543
     * @param container to watch
 
544
     */
 
545
    private async void wait_for_item (WritableContainer container) {
 
546
        debug ("Waiting for new item to appear under container '%s'..",
 
547
               container.id);
 
548
 
 
549
        MediaItem item = null;
 
550
 
 
551
        while (item == null) {
 
552
            try {
 
553
                item = (yield container.find_object (this.item.id,
 
554
                                                     this.cancellable))
 
555
                       as MediaItem;
 
556
            } catch (Error error) {
 
557
                warning ("Error from container '%s' on trying to find newly " +
 
558
                         "added child item '%s' in it",
 
559
                         container.id,
 
560
                         this.item.id);
 
561
            }
 
562
 
 
563
            if (item == null) {
 
564
                var id = container.container_updated.connect ((container) => {
 
565
                    this.wait_for_item.callback ();
 
566
                });
 
567
 
 
568
                uint timeout = 0;
 
569
                timeout = Timeout.add_seconds (5, () => {
 
570
                    debug ("Timeout on waiting for 'updated' signal on '%s'.",
 
571
                           container.id);
 
572
                    timeout = 0;
 
573
                    this.wait_for_item.callback ();
 
574
 
 
575
                    return false;
 
576
                });
 
577
 
 
578
                yield;
 
579
 
 
580
                container.disconnect (id);
 
581
 
 
582
                if (timeout != 0) {
 
583
                    Source.remove (timeout);
 
584
                } else {
 
585
                    break;
 
586
                }
 
587
            }
 
588
        }
 
589
        debug ("Finished waiting for new item to appear under container '%s'",
 
590
               container.id);
 
591
    }
 
592
 
 
593
    /**
 
594
     * Check if the profile is supported.
 
595
     *
 
596
     * The check is performed against GUPnP-DLNA's database explicitly excluding
 
597
     * the transcoders.
 
598
     *
 
599
     * @param profile to check
 
600
     * @returns true if the profile is supported, false otherwise.
 
601
     */
 
602
    private bool is_profile_valid (string profile) {
 
603
        var discoverer = new GUPnP.DLNADiscoverer ((ClockTime) SECOND,
 
604
                                                   true,
 
605
                                                   false);
 
606
 
 
607
        var valid = false;
 
608
        foreach (var known_profile in discoverer.list_profiles ()) {
 
609
            if (known_profile.name == profile) {
 
610
                valid = true;
 
611
 
 
612
                break;
 
613
            }
 
614
        }
 
615
 
 
616
        return valid;
 
617
    }
 
618
}