2
* Copyright (C) 2010 Nokia Corporation.
4
* Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
6
* This file is part of Rygel.
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.
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.
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.
27
* CreateObject action implementation.
29
internal class Rygel.ItemCreator: GLib.Object, Rygel.StateMachine {
30
private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");
32
private const string INVALID_CHARS = "/?<>\\:*|\"";
35
private string container_id;
36
private string elements;
38
private DIDLLiteItem didl_item;
39
private MediaItem item;
41
private ContentDirectory content_dir;
42
private ServiceAction action;
43
private DIDLLiteWriter didl_writer;
44
private DIDLLiteParser didl_parser;
45
private Regex title_regex;
47
public Cancellable cancellable { get; set; }
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 ();
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 (); }
64
public async void run () {
69
var container = yield this.fetch_container ();
71
/* Verify the create class. Note that we always assume
72
* createClass@includeDerived to be false.
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.
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,
87
yield this.create_item_from_didl (container);
88
yield container.add_item (this.item, this.cancellable);
90
yield this.wait_for_item (container);
92
this.item.serialize (didl_writer, this.content_dir.http_server);
94
// Conclude the successful action
97
if (this.container_id == "DLNA.ORG_AnyContainer" &&
98
this.item.place_holder) {
99
var queue = ItemRemovalQueue.get_default ();
101
queue.queue (this.item, this.cancellable);
103
} catch (Error err) {
104
this.handle_error (err);
109
* Check the supplied input parameters.
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);
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"));
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"));
132
* Parse the given DIDL-Lite snippet.
134
* Parses the DIDL-Lite and performs checking of the passed meta-data
135
* according to UPnP and DLNA guidelines.
137
private void parse_didl () throws Error {
138
this.didl_parser.item_available.connect ((didl_item) => {
139
this.didl_item = didl_item;
143
this.didl_parser.parse_didl (this.elements);
144
} catch (Error parse_err) {
145
throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
148
if (this.didl_item == null) {
149
var message = _("No items in DIDL-Lite from client: '%s'");
151
throw new ContentDirectoryError.BAD_METADATA
152
(message, this.elements);
155
if (didl_item.id == null || didl_item.id != "") {
156
throw new ContentDirectoryError.BAD_METADATA
157
("@id must be set to \"\" in " +
161
if (didl_item.title == null) {
162
throw new ContentDirectoryError.BAD_METADATA
163
("dc:title must be set in " +
167
// FIXME: Is this check really necessary? 7.3.118.4 passes without it.
168
if ((didl_item.dlna_managed &
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'");
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 ");
184
if (didl_item.restricted) {
185
throw new ContentDirectoryError.INVALID_ARGS
186
("Cannot create restricted item");
191
* Modify the give UPnP class to be a more general one.
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
197
* @param upnp_class the current UPnP class which will be modified in-place.
199
private void generalize_upnp_class (ref string upnp_class) {
200
char *needle = upnp_class.rstr_len (-1, ".");
201
if (needle != null) {
207
* Find a container that can create items matching the UPnP class of the
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.
213
* @returns a container able to create the item or null if no such container
216
private async MediaObject? find_any_container () throws Error {
217
var root_container = this.content_dir.root_container
218
as SearchableContainer;
220
if (root_container == null) {
224
var upnp_class = this.didl_item.upnp_class;
226
var expression = new RelationalExpression ();
227
expression.op = SearchCriteriaOp.DERIVED_FROM;
228
expression.operand1 = "upnp:createClass";
230
while (upnp_class != "object.item") {
231
expression.operand2 = upnp_class;
234
var result = yield root_container.search (expression,
238
root_container.sort_criteria,
240
if (result.size > 0) {
241
this.didl_item.upnp_class = upnp_class;
245
this.generalize_upnp_class (ref upnp_class);
249
if (upnp_class == "object.item") {
250
throw new ContentDirectoryError.BAD_METADATA
251
("'%s' UPnP class unsupported",
252
this.didl_item.upnp_class);
259
* Get the container to create the item in.
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"
265
* @return a instance of WritableContainer matching the criteria
266
* @throws ContentDirectoryError for various problems
268
private async WritableContainer fetch_container () throws Error {
269
MediaObject media_object = null;
271
if (this.container_id == "DLNA.ORG_AnyContainer") {
272
media_object = yield this.find_any_container ();
274
media_object = yield this.content_dir.root_container.find_object
275
(this.container_id, this.cancellable);
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"),
288
// FIXME: Check for @restricted=1 missing?
290
return media_object as WritableContainer;
293
private void conclude () {
294
/* Retrieve generated string */
295
string didl = this.didl_writer.get_string ();
297
/* Set action return arguments */
298
this.action.set ("ObjectID", typeof (string), this.item.id,
299
"Result", typeof (string), didl);
301
this.action.return ();
305
private void handle_error (Error error) {
306
if (error is ContentDirectoryError) {
307
this.action.return_error (error.code, error.message);
309
this.action.return_error (701, error.message);
312
warning (_("Failed to create item under '%s': %s"),
319
private string get_generic_mime_type () {
320
if (this.item is ImageItem) {
322
} else if (this.item is VideoItem) {
330
* Transfer information passed by caller to a MediaItem.
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.
337
* This function fills ItemCreator.item.
339
private async void create_item_from_didl (WritableContainer container)
341
this.item = this.create_item (this.didl_item.id,
343
this.didl_item.title,
344
this.didl_item.upnp_class);
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;
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",
359
this.item.dlna_profile = info.dlna_profile;
362
if (info.mime_type != null) {
363
this.item.mime_type = info.mime_type;
367
string sanitized_uri;
368
if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
369
this.item.add_uri (sanitized_uri);
372
if (resource.size >= 0) {
373
this.item.size = resource.size;
377
if (this.item.mime_type == null) {
378
this.item.mime_type = this.get_generic_mime_type ();
381
if (this.item.size < 0) {
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;
390
var file = File.new_for_uri (this.item.uris[0]);
391
this.item.place_holder = !file.is_native ();
394
this.item.id = this.item.uris[0];
396
this.parse_and_verify_didl_date ();
399
private void parse_and_verify_didl_date () throws Error {
400
if (this.didl_item.date == null) {
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);
411
int year = 0, month = 0, day = 0;
413
if (this.didl_item.date.scanf ("%4d-%02d-%02d",
417
throw new ContentDirectoryError.BAD_METADATA
418
("Invalid date format: %s",
419
this.didl_item.date);
422
var date = GLib.Date ();
423
date.set_dmy ((DateDay) day, (DateMonth) month, (DateYear) year);
425
if (!date.valid ()) {
426
throw new ContentDirectoryError.BAD_METADATA
428
this.didl_item.date);
431
this.item.date = this.didl_item.date + "T00:00:00";
434
private MediaItem create_item (string id,
435
WritableContainer parent,
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);
450
throw new ContentDirectoryError.BAD_METADATA
451
("Creation of item of class '%s' " +
458
* Simple check for the validity of an URI.
460
* Check is done by parsing the URI with soup. Additionaly a cleaned-up
461
* version of the URI is returned in sanitized_uri.
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.
467
private bool is_valid_uri (string? uri, out string sanitized_uri) {
468
sanitized_uri = null;
469
if (uri == null || uri == "") {
473
var soup_uri = new Soup.URI (uri);
475
if (soup_uri == null || soup_uri.scheme == null) {
479
sanitized_uri = soup_uri.to_string (false);
485
* Transform the title to be usable on legacy file-systems such as FAT32.
487
* The function trims down the title to 205 chars (leaving room for an UUID)
488
* and replaces all special characters.
490
* @param title of the the media item
491
* @return the cleaned and shortened title
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,
499
RegexMatchFlags.NOTEMPTY);
501
var udn = new uchar[50];
502
var id = new uchar[16];
505
UUID.unparse (id, udn);
507
return (string) udn + "-" + mangled;
511
* Create an URI from the item's title.
513
* Create an unique URI from the supplied title by cleaning it from
514
* unwanted characters, shortening it and adding an UUID.
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
520
private async string create_uri (WritableContainer container, string title)
522
var dir = yield container.get_writable (this.cancellable);
524
throw new ContentDirectoryError.RESTRICTED_PARENT
525
(_("Object creation in %s not allowed"),
529
var file = dir.get_child_for_display_name (this.mangle_title (title));
531
return file.get_uri ();
535
* Wait for the new item
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.
543
* @param container to watch
545
private async void wait_for_item (WritableContainer container) {
546
debug ("Waiting for new item to appear under container '%s'..",
549
MediaItem item = null;
551
while (item == null) {
553
item = (yield container.find_object (this.item.id,
556
} catch (Error error) {
557
warning ("Error from container '%s' on trying to find newly " +
558
"added child item '%s' in it",
564
var id = container.container_updated.connect ((container) => {
565
this.wait_for_item.callback ();
569
timeout = Timeout.add_seconds (5, () => {
570
debug ("Timeout on waiting for 'updated' signal on '%s'.",
573
this.wait_for_item.callback ();
580
container.disconnect (id);
583
Source.remove (timeout);
589
debug ("Finished waiting for new item to appear under container '%s'",
594
* Check if the profile is supported.
596
* The check is performed against GUPnP-DLNA's database explicitly excluding
599
* @param profile to check
600
* @returns true if the profile is supported, false otherwise.
602
private bool is_profile_valid (string profile) {
603
var discoverer = new GUPnP.DLNADiscoverer ((ClockTime) SECOND,
608
foreach (var known_profile in discoverer.list_profiles ()) {
609
if (known_profile.name == profile) {