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.
26 private errordomain Rygel.ItemCreatorError {
31 * CreateObject action implementation.
33 internal class Rygel.ItemCreator: GLib.Object, Rygel.StateMachine {
34 private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");
36 private const string INVALID_CHARS = "/?<>\\:*|\"";
39 private string container_id;
40 private string elements;
42 private DIDLLiteItem didl_item;
43 private MediaItem item;
45 private ContentDirectory content_dir;
46 private ServiceAction action;
47 private DIDLLiteWriter didl_writer;
48 private DIDLLiteParser didl_parser;
49 private Regex title_regex;
51 public Cancellable cancellable { get; set; }
53 public ItemCreator (ContentDirectory content_dir,
54 owned ServiceAction action) {
55 this.content_dir = content_dir;
56 this.cancellable = content_dir.cancellable;
57 this.action = (owned) action;
58 this.didl_writer = new DIDLLiteWriter (null);
59 this.didl_parser = new DIDLLiteParser ();
61 var pattern = "[" + Regex.escape_string (INVALID_CHARS) + "]";
62 this.title_regex = new Regex (pattern,
63 RegexCompileFlags.OPTIMIZE,
64 RegexMatchFlags.NOTEMPTY);
65 } catch (Error error) { assert_not_reached (); }
68 public async void run () {
73 var container = yield this.fetch_container ();
75 /* Verify the create class. Note that we always assume
76 * createClass@includeDerived to be false.
78 * DLNA_ORG.AnyContainer is a special case. We are allowed to
79 * modify the UPnP class to something we support and
80 * fetch_container took care of this already.
82 if (!container.can_create (this.didl_item.upnp_class) &&
83 this.container_id != "DLNA_ORG.AnyContainer") {
84 throw new ContentDirectoryError.BAD_METADATA
85 ("Creating of objects with class %s " +
86 "is not supported in %s",
87 this.didl_item.upnp_class,
91 yield this.create_item_from_didl (container);
92 yield container.add_item (this.item, this.cancellable);
94 yield this.wait_for_item (container);
96 this.item.serialize (didl_writer, this.content_dir.http_server);
98 // Conclude the successful action
101 if (this.container_id == "DLNA.ORG_AnyContainer" &&
102 this.item.place_holder) {
103 var queue = ItemRemovalQueue.get_default ();
105 queue.queue (this.item, this.cancellable);
107 } catch (Error err) {
108 this.handle_error (err);
112 private void parse_args () throws Error {
113 /* Start by parsing the 'in' arguments */
114 this.action.get ("ContainerID", typeof (string), out this.container_id,
115 "Elements", typeof (string), out this.elements);
117 if (this.elements == null) {
118 throw new ContentDirectoryError.BAD_METADATA
119 (_("'Elements' argument missing."));
120 } else if (comment_pattern.match_string (this.elements)) {
121 throw new ContentDirectoryError.BAD_METADATA
122 (_("Comments not allowed in XML"));
125 if (this.container_id == null) {
126 // Sorry we can't do anything without ContainerID
127 throw new ContentDirectoryError.NO_SUCH_OBJECT
128 (_("No such object"));
132 private void parse_didl () throws Error {
133 this.didl_parser.item_available.connect ((didl_item) => {
134 this.didl_item = didl_item;
138 this.didl_parser.parse_didl (this.elements);
139 } catch (Error parse_err) {
140 throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
143 if (this.didl_item == null) {
144 var message = _("No items in DIDL-Lite from client: '%s'");
146 throw new ItemCreatorError.PARSE (message, this.elements);
149 if (didl_item.id == null || didl_item.id != "") {
150 throw new ContentDirectoryError.BAD_METADATA
151 ("@id must be set to \"\" in " +
155 if (didl_item.title == null) {
156 throw new ContentDirectoryError.BAD_METADATA
157 ("dc:title must be set in " +
161 // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
162 if ((didl_item.dlna_managed &
164 OCMFlags.CREATE_CONTAINER |
165 OCMFlags.UPLOAD_DESTROYABLE)) != 0) {
166 throw new ContentDirectoryError.BAD_METADATA
167 ("Flags that must not be set " +
168 "were found in 'dlnaManaged'");
171 if (didl_item.upnp_class == null ||
172 didl_item.upnp_class == "" ||
173 !didl_item.upnp_class.has_prefix ("object.item")) {
174 throw new ContentDirectoryError.BAD_METADATA
175 ("Invalid upnp:class given ");
178 if (didl_item.restricted) {
179 throw new ContentDirectoryError.INVALID_ARGS
180 ("Cannot create restricted item");
184 private void generalize_upnp_class (ref string upnp_class) {
185 char *needle = upnp_class.rstr_len (-1, ".");
186 if (needle != null) {
192 * Find a container that can create items matching the UPnP class of the
195 * If the item's UPnP class cannot be found, generalize the UPnP class until
196 * we reach object.item according to DLNA guideline 7.3.120.4.
198 * @returns a container able to create the item or null if no such container
201 private async MediaObject? find_any_container () throws Error {
202 var root_container = this.content_dir.root_container
203 as SearchableContainer;
205 if (root_container == null) {
209 var upnp_class = this.didl_item.upnp_class;
211 var expression = new RelationalExpression ();
212 expression.op = SearchCriteriaOp.DERIVED_FROM;
213 expression.operand1 = "upnp:createClass";
215 while (upnp_class != "object.item") {
216 expression.operand2 = upnp_class;
219 var result = yield root_container.search (expression,
224 if (result.size > 0) {
225 this.didl_item.upnp_class = upnp_class;
229 this.generalize_upnp_class (ref upnp_class);
233 if (upnp_class == "object.item") {
234 throw new ContentDirectoryError.BAD_METADATA
235 ("'%s' UPnP class unsupported",
236 this.didl_item.upnp_class);
242 private async WritableContainer fetch_container () throws Error {
243 MediaObject media_object = null;
245 if (this.container_id == "DLNA.ORG_AnyContainer") {
246 media_object = yield this.find_any_container ();
248 media_object = yield this.content_dir.root_container.find_object
249 (this.container_id, this.cancellable);
252 if (media_object == null || !(media_object is MediaContainer)) {
253 throw new ContentDirectoryError.NO_SUCH_OBJECT
254 (_("No such object"));
255 } else if (!(OCMFlags.UPLOAD in media_object.ocm_flags) ||
256 !(media_object is WritableContainer)) {
257 throw new ContentDirectoryError.RESTRICTED_PARENT
258 (_("Object creation in %s not allowed"),
262 // FIXME: Check for @restricted=1 missing?
264 return media_object as WritableContainer;
267 private void conclude () {
268 /* Retrieve generated string */
269 string didl = this.didl_writer.get_string ();
271 /* Set action return arguments */
272 this.action.set ("ObjectID", typeof (string), this.item.id,
273 "Result", typeof (string), didl);
275 this.action.return ();
279 private void handle_error (Error error) {
280 if (error is ContentDirectoryError) {
281 this.action.return_error (error.code, error.message);
283 this.action.return_error (701, error.message);
286 warning (_("Failed to create item under '%s': %s"),
293 private string get_generic_mime_type () {
294 if (this.item is ImageItem) {
296 } else if (this.item is VideoItem) {
303 private async void create_item_from_didl (WritableContainer container)
305 this.item = this.create_item (this.didl_item.id,
307 this.didl_item.title,
308 this.didl_item.upnp_class);
310 var resources = this.didl_item.get_resources ();
311 if (resources != null && resources.length () > 0) {
312 var resource = resources.nth (0).data;
313 var info = resource.protocol_info;
316 if (info.dlna_profile != null) {
317 if (!this.is_profile_valid (info.dlna_profile)) {
318 throw new ContentDirectoryError.BAD_METADATA
319 ("'%s' DLNA profile unsupported",
323 this.item.dlna_profile = info.dlna_profile;
326 if (info.mime_type != null) {
327 this.item.mime_type = info.mime_type;
331 string sanitized_uri;
332 if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
333 this.item.add_uri (sanitized_uri);
336 if (resource.size >= 0) {
337 this.item.size = resource.size;
341 if (this.item.mime_type == null) {
342 this.item.mime_type = this.get_generic_mime_type ();
345 if (this.item.size < 0) {
349 if (this.item.uris.size == 0) {
350 var uri = yield this.create_uri (container, this.item.title);
351 this.item.uris.add (uri);
352 this.item.place_holder = true;
354 var file = File.new_for_uri (this.item.uris[0]);
355 this.item.place_holder = !file.is_native ();
358 this.item.id = this.item.uris[0];
361 private MediaItem create_item (string id,
362 WritableContainer parent,
364 string upnp_class) throws Error {
365 switch (upnp_class) {
366 case ImageItem.UPNP_CLASS:
367 return new ImageItem (id, parent, title);
368 case PhotoItem.UPNP_CLASS:
369 return new PhotoItem (id, parent, title);
370 case VideoItem.UPNP_CLASS:
371 return new VideoItem (id, parent, title);
372 case AudioItem.UPNP_CLASS:
373 return new AudioItem (id, parent, title);
374 case MusicItem.UPNP_CLASS:
375 return new MusicItem (id, parent, title);
377 throw new ContentDirectoryError.BAD_METADATA
378 ("Creation of item of class '%s' " +
384 private bool is_valid_uri (string? uri, out string sanitized_uri) {
385 sanitized_uri = null;
386 if (uri == null || uri == "") {
390 var soup_uri = new Soup.URI (uri);
392 if (soup_uri == null || soup_uri.scheme == null) {
396 sanitized_uri = soup_uri.to_string (false);
401 private string mangle_title (string title) throws Error {
402 var mangled = title.substring (0, int.min (title.length, 205));
403 mangled = this.title_regex.replace_literal (mangled,
407 RegexMatchFlags.NOTEMPTY);
412 private async string create_uri (WritableContainer container, string title)
414 var dir = yield container.get_writable (this.cancellable);
416 throw new ContentDirectoryError.RESTRICTED_PARENT
417 (_("Object creation in %s not allowed"),
421 var file = dir.get_child_for_display_name (this.mangle_title (title));
423 var udn = new uchar[50];
424 var id = new uchar[16];
427 uuid_unparse (id, udn);
429 return file.get_uri () + (string) udn;
432 private async void wait_for_item (WritableContainer container) {
433 debug ("Waiting for new item to appear under container '%s'..",
436 MediaItem item = null;
438 while (item == null) {
440 item = (yield container.find_object (this.item.id,
443 } catch (Error error) {
444 warning ("Error from container '%s' on trying to find newly " +
445 "added child item '%s' in it",
451 var id = container.container_updated.connect ((container) => {
452 this.wait_for_item.callback ();
456 timeout = Timeout.add_seconds (5, () => {
457 debug ("Timeout on waiting for 'updated' signal on '%s'.",
460 this.wait_for_item.callback ();
467 container.disconnect (id);
470 Source.remove (timeout);
476 debug ("Finished waiting for new item to appear under container '%s'",
480 private bool is_profile_valid (string profile) {
481 var discoverer = new GUPnP.DLNADiscoverer ((ClockTime) SECOND,
486 foreach (var known_profile in discoverer.list_profiles ()) {
487 if (known_profile.name == profile) {