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);
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);
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"));
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"));
128 private void parse_didl () throws Error {
129 this.didl_parser.item_available.connect ((didl_item) => {
130 this.didl_item = didl_item;
134 this.didl_parser.parse_didl (this.elements);
135 } catch (Error parse_err) {
136 throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
139 if (this.didl_item == null) {
140 var message = _("No items in DIDL-Lite from client: '%s'");
142 throw new ContentDirectoryError.BAD_METADATA
143 (message, this.elements);
146 if (didl_item.id == null || didl_item.id != "") {
147 throw new ContentDirectoryError.BAD_METADATA
148 ("@id must be set to \"\" in " +
152 if (didl_item.title == null) {
153 throw new ContentDirectoryError.BAD_METADATA
154 ("dc:title must be set in " +
158 // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
159 if ((didl_item.dlna_managed &
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'");
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 ");
175 if (didl_item.restricted) {
176 throw new ContentDirectoryError.INVALID_ARGS
177 ("Cannot create restricted item");
181 private void generalize_upnp_class (ref string upnp_class) {
182 char *needle = upnp_class.rstr_len (-1, ".");
183 if (needle != null) {
189 * Find a container that can create items matching the UPnP class of the
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.
195 * @returns a container able to create the item or null if no such container
198 private async MediaObject? find_any_container () throws Error {
199 var root_container = this.content_dir.root_container
200 as SearchableContainer;
202 if (root_container == null) {
206 var upnp_class = this.didl_item.upnp_class;
208 var expression = new RelationalExpression ();
209 expression.op = SearchCriteriaOp.DERIVED_FROM;
210 expression.operand1 = "upnp:createClass";
212 while (upnp_class != "object.item") {
213 expression.operand2 = upnp_class;
216 var result = yield root_container.search (expression,
221 if (result.size > 0) {
222 this.didl_item.upnp_class = upnp_class;
226 this.generalize_upnp_class (ref upnp_class);
230 if (upnp_class == "object.item") {
231 throw new ContentDirectoryError.BAD_METADATA
232 ("'%s' UPnP class unsupported",
233 this.didl_item.upnp_class);
239 private async WritableContainer fetch_container () throws Error {
240 MediaObject media_object = null;
242 if (this.container_id == "DLNA.ORG_AnyContainer") {
243 media_object = yield this.find_any_container ();
245 media_object = yield this.content_dir.root_container.find_object
246 (this.container_id, this.cancellable);
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"),
259 // FIXME: Check for @restricted=1 missing?
261 return media_object as WritableContainer;
264 private void conclude () {
265 /* Retrieve generated string */
266 string didl = this.didl_writer.get_string ();
268 /* Set action return arguments */
269 this.action.set ("ObjectID", typeof (string), this.item.id,
270 "Result", typeof (string), didl);
272 this.action.return ();
276 private void handle_error (Error error) {
277 if (error is ContentDirectoryError) {
278 this.action.return_error (error.code, error.message);
280 this.action.return_error (701, error.message);
283 warning (_("Failed to create item under '%s': %s"),
290 private string get_generic_mime_type () {
291 if (this.item is ImageItem) {
293 } else if (this.item is VideoItem) {
300 private async void create_item_from_didl (WritableContainer container)
302 this.item = this.create_item (this.didl_item.id,
304 this.didl_item.title,
305 this.didl_item.upnp_class);
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;
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",
320 this.item.dlna_profile = info.dlna_profile;
323 if (info.mime_type != null) {
324 this.item.mime_type = info.mime_type;
328 string sanitized_uri;
329 if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
330 this.item.add_uri (sanitized_uri);
333 if (resource.size >= 0) {
334 this.item.size = resource.size;
338 if (this.item.mime_type == null) {
339 this.item.mime_type = this.get_generic_mime_type ();
342 if (this.item.size < 0) {
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;
351 var file = File.new_for_uri (this.item.uris[0]);
352 this.item.place_holder = !file.is_native ();
355 this.item.id = this.item.uris[0];
358 private MediaItem create_item (string id,
359 WritableContainer parent,
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);
374 throw new ContentDirectoryError.BAD_METADATA
375 ("Creation of item of class '%s' " +
381 private bool is_valid_uri (string? uri, out string sanitized_uri) {
382 sanitized_uri = null;
383 if (uri == null || uri == "") {
387 var soup_uri = new Soup.URI (uri);
389 if (soup_uri == null || soup_uri.scheme == null) {
393 sanitized_uri = soup_uri.to_string (false);
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,
404 RegexMatchFlags.NOTEMPTY);
409 private async string create_uri (WritableContainer container, string title)
411 var dir = yield container.get_writable (this.cancellable);
413 throw new ContentDirectoryError.RESTRICTED_PARENT
414 (_("Object creation in %s not allowed"),
418 var file = dir.get_child_for_display_name (this.mangle_title (title));
420 var udn = new uchar[50];
421 var id = new uchar[16];
424 uuid_unparse (id, udn);
426 return file.get_uri () + (string) udn;
429 private async void wait_for_item (WritableContainer container) {
430 debug ("Waiting for new item to appear under container '%s'..",
433 MediaItem item = null;
435 while (item == null) {
437 item = (yield container.find_object (this.item.id,
440 } catch (Error error) {
441 warning ("Error from container '%s' on trying to find newly " +
442 "added child item '%s' in it",
448 var id = container.container_updated.connect ((container) => {
449 this.wait_for_item.callback ();
453 timeout = Timeout.add_seconds (5, () => {
454 debug ("Timeout on waiting for 'updated' signal on '%s'.",
457 this.wait_for_item.callback ();
464 container.disconnect (id);
467 Source.remove (timeout);
473 debug ("Finished waiting for new item to appear under container '%s'",
477 private bool is_profile_valid (string profile) {
478 var discoverer = new GUPnP.DLNADiscoverer ((ClockTime) SECOND,
483 foreach (var known_profile in discoverer.list_profiles ()) {
484 if (known_profile.name == profile) {