2 * Copyright (C) 2010-2011 Nokia Corporation.
3 * Copyright (C) 2012 Intel Corporation.
5 * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
6 * Jens Georg <jensg@openismus.com>
8 * This file is part of Rygel.
10 * Rygel is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU Lesser General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * Rygel is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Lesser General Public License for more details.
20 * You should have received a copy of the GNU Lesser General Public License
21 * along with this program; if not, write to the Free Software Foundation,
22 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
28 * Dummy implementation of Rygel.MediaContainer to pass on to
29 * Rygel.WritableContianer for creation.
31 private class Rygel.BaseMediaContainer : MediaContainer {
33 * Create a media container with the specified details.
35 * @param id See the id property of the #RygelMediaObject class.
36 * @param parent The parent container, if any.
37 * @param title See the title property of the #RygelMediaObject class.
38 * @param child_count The initially-known number of child items.
40 public BaseMediaContainer (string id,
41 MediaContainer? parent,
47 child_count : child_count);
51 * Fetches the list of media objects directly under this container.
53 * @param offset zero-based index of the first item to return
54 * @param max_count maximum number of objects to return
55 * @param sort_criteria sorting order of objects to return
56 * @param cancellable optional cancellable for this operation
58 * @return A list of media objects.
60 public override async MediaObjects? get_children
64 Cancellable? cancellable)
70 * Recursively searches this container for a media object with the given ID.
72 * @param id ID of the media object to search for
73 * @param cancellable optional cancellable for this operation
75 * @return the found media object.
77 public override async MediaObject? find_object (string id,
78 Cancellable? cancellable)
88 * CreateObject action implementation.
90 internal class Rygel.ObjectCreator: GLib.Object, Rygel.StateMachine {
91 private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");
93 private const string INVALID_CHARS = "/?<>\\:*|\"";
96 private string container_id;
97 private string elements;
99 private DIDLLiteObject didl_object;
100 private MediaObject object;
102 private ContentDirectory content_dir;
103 private ServiceAction action;
104 private Serializer serializer;
105 private DIDLLiteParser didl_parser;
106 private Regex title_regex;
108 public Cancellable cancellable { get; set; }
110 public ObjectCreator (ContentDirectory content_dir,
111 owned ServiceAction action) {
112 this.content_dir = content_dir;
113 this.cancellable = content_dir.cancellable;
114 this.action = (owned) action;
115 this.serializer = new Serializer (SerializerType.GENERIC_DIDL);
116 this.didl_parser = new DIDLLiteParser ();
118 var pattern = "[" + Regex.escape_string (INVALID_CHARS) + "]";
119 this.title_regex = new Regex (pattern,
120 RegexCompileFlags.OPTIMIZE,
121 RegexMatchFlags.NOTEMPTY);
122 } catch (Error error) { assert_not_reached (); }
125 public async void run () {
130 var container = yield this.fetch_container ();
132 /* Verify the create class. Note that we always assume
133 * createClass@includeDerived to be false.
135 * DLNA.ORG_AnyContainer is a special case. We are allowed to
136 * modify the UPnP class to something we support and
137 * fetch_container took care of this already.
139 if (!container.can_create (this.didl_object.upnp_class) &&
140 this.container_id != MediaContainer.ANY) {
141 throw new ContentDirectoryError.BAD_METADATA
142 ("Creating of objects with class %s " +
143 "is not supported in %s",
144 this.didl_object.upnp_class,
148 if (this.didl_object is DIDLLiteContainer &&
149 !this.validate_create_class (container)) {
150 throw new ContentDirectoryError.BAD_METADATA
151 (_("upnp:createClass value not supported"));
154 yield this.create_object_from_didl (container);
155 if (this.object is MediaItem) {
156 yield container.add_item (this.object as MediaItem,
159 yield container.add_container (this.object as MediaContainer,
163 yield this.wait_for_object (container);
165 this.object.serialize (serializer, this.content_dir.http_server);
167 // Conclude the successful action
170 if (this.container_id == MediaContainer.ANY &&
171 (this.object is MediaItem && (this.object as
172 MediaItem).place_holder)) {
173 var queue = ObjectRemovalQueue.get_default ();
175 queue.queue (this.object, this.cancellable);
177 } catch (Error err) {
178 this.handle_error (err);
183 * Check the supplied input parameters.
185 private void parse_args () throws Error {
186 /* Start by parsing the 'in' arguments */
187 this.action.get ("ContainerID", typeof (string), out this.container_id,
188 "Elements", typeof (string), out this.elements);
190 if (this.elements == null) {
191 throw new ContentDirectoryError.BAD_METADATA
192 (_("'Elements' argument missing."));
193 } else if (comment_pattern.match_string (this.elements)) {
194 throw new ContentDirectoryError.BAD_METADATA
195 (_("Comments not allowed in XML"));
198 if (this.container_id == null) {
199 // Sorry we can't do anything without ContainerID
200 throw new ContentDirectoryError.INVALID_ARGS
201 (_("Missing ContainerID argument"));
206 * Parse the given DIDL-Lite snippet.
208 * Parses the DIDL-Lite and performs checking of the passed meta-data
209 * according to UPnP and DLNA guidelines.
211 private void parse_didl () throws Error {
212 // FIXME: This will take the last object in the DIDL-Lite, maybe we
213 // should limit it to one somehow.
214 this.didl_parser.object_available.connect ((didl_object) => {
215 this.didl_object = didl_object;
219 this.didl_parser.parse_didl (this.elements);
220 } catch (Error parse_err) {
221 throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
224 if (this.didl_object == null) {
225 var message = _("No objects in DIDL-Lite from client: '%s'");
227 throw new ContentDirectoryError.BAD_METADATA
228 (message, this.elements);
231 if (didl_object.id == null || didl_object.id != "") {
232 var msg = _("@id must be set to \"\" in CreateObject call");
233 throw new ContentDirectoryError.BAD_METADATA (msg);
236 if (didl_object.title == null) {
237 var msg = _("dc:title must not be empty in CreateObject call");
238 throw new ContentDirectoryError.BAD_METADATA (msg);
241 // FIXME: Is this check really necessary? 7.3.118.4 passes without it.
242 // These flags must not be set on items.
243 if (didl_object is DIDLLiteItem &&
244 ((didl_object.dlna_managed &
246 OCMFlags.CREATE_CONTAINER |
247 OCMFlags.UPLOAD_DESTROYABLE)) != 0)) {
248 var msg = _("Flags that must not be set were found in 'dlnaManaged'");
249 throw new ContentDirectoryError.BAD_METADATA (msg);
252 if (didl_object.upnp_class == null ||
253 didl_object.upnp_class == "" ||
254 !didl_object.upnp_class.has_prefix ("object")) {
255 throw new ContentDirectoryError.BAD_METADATA
256 (_("Invalid upnp:class given in CreateObject"));
259 if (didl_object.restricted) {
260 throw new ContentDirectoryError.BAD_METADATA
261 (_("Cannot create restricted item"));
264 // Handle DIDL_S items...
265 if (this.didl_object.upnp_class == "object.item") {
266 var resources = this.didl_object.get_resources ();
267 if (resources != null &&
268 resources.data.protocol_info.dlna_profile == "DIDL_S") {
269 this.didl_object.upnp_class = PlaylistItem.UPNP_CLASS;
275 * Modify the give UPnP class to be a more general one.
277 * Used to simplify the search for a valid container in the
278 * DLNA.ORG_AnyContainer use-case.
279 * Example: object.item.videoItem.videoBroadcast → object.item.videoItem
281 * @param upnp_class the current UPnP class which will be modified in-place.
283 private void generalize_upnp_class (ref string upnp_class) {
284 char *needle = upnp_class.rstr_len (-1, ".");
285 if (needle != null) {
290 private async SearchExpression build_create_class_expression
291 (SearchExpression expression) {
292 // Take create-classes into account
293 if (!(this.didl_object is DIDLLiteContainer)) {
297 var didl_container = this.didl_object as DIDLLiteContainer;
298 var create_classes = didl_container.get_create_classes ();
299 if (create_classes == null) {
303 var builder = new StringBuilder ("(");
304 foreach (var create_class in create_classes) {
305 builder.append_printf ("(upnp:createClass derivedfrom \"%s\") AND",
309 // remove dangeling AND
310 builder.truncate (builder.len - 3);
311 builder.append (")");
314 var parser = new Rygel.SearchCriteriaParser (builder.str);
317 var rel = new LogicalExpression ();
318 rel.operand1 = expression;
319 rel.op = LogicalOperator.AND;
320 rel.operand2 = parser.expression;
323 } catch (Error error) {
324 assert_not_reached ();
329 * Find a container that can create items matching the UPnP class of the
332 * If the item's UPnP class cannot be found, generalize the UPnP class until
333 * we reach object.item according to DLNA guideline 7.3.120.4.
335 * @returns a container able to create the item or null if no such container
338 private async MediaObject? find_any_container () throws Error {
339 var root_container = this.content_dir.root_container
340 as SearchableContainer;
342 if (root_container == null) {
346 var upnp_class = this.didl_object.upnp_class;
348 var expression = new RelationalExpression ();
349 expression.op = SearchCriteriaOp.DERIVED_FROM;
350 expression.operand1 = "upnp:createClass";
352 // Add container's create classes to the search expression if there
354 var search_expression = yield this.build_create_class_expression
357 while (upnp_class != "object") {
358 expression.operand2 = upnp_class;
361 var result = yield root_container.search (search_expression,
365 root_container.sort_criteria,
367 if (result.size > 0) {
368 this.didl_object.upnp_class = upnp_class;
372 this.generalize_upnp_class (ref upnp_class);
376 if (upnp_class == "object") {
377 throw new ContentDirectoryError.BAD_METADATA
378 (_("UPnP class '%s' not supported"),
379 this.didl_object.upnp_class);
386 * Get the container to create the item in.
388 * This will either try to fetch the container supplied by the caller or
389 * search for a container if the caller supplied the "DLNA.ORG_AnyContainer"
392 * @return an instance of WritableContainer matching the criteria
393 * @throws ContentDirectoryError for various problems
395 private async WritableContainer fetch_container () throws Error {
396 MediaObject media_object = null;
398 if (this.container_id == MediaContainer.ANY) {
399 media_object = yield this.find_any_container ();
401 media_object = yield this.content_dir.root_container.find_object
402 (this.container_id, this.cancellable);
405 if (media_object == null || !(media_object is MediaContainer)) {
406 throw new ContentDirectoryError.NO_SUCH_CONTAINER
407 (_("No such container"));
410 if (!(media_object is WritableContainer)) {
411 throw new ContentDirectoryError.RESTRICTED_PARENT
412 (_("Object creation in %s not allowed"),
416 // If the object to be created is an item, ocm_flags must contain
417 // OCMFlags.UPLOAD, it it's a container, ocm_flags must contain
418 // OCMFlags.CREATE_CONTAINER
419 if (!((this.didl_object is DIDLLiteItem &&
420 (OCMFlags.UPLOAD in media_object.ocm_flags)) ||
421 (this.didl_object is DIDLLiteContainer &&
422 (OCMFlags.CREATE_CONTAINER in media_object.ocm_flags)))) {
423 throw new ContentDirectoryError.RESTRICTED_PARENT
424 (_("Object creation in %s not allowed"),
428 // FIXME: Check for @restricted=1 missing?
430 return media_object as WritableContainer;
433 private void conclude () {
434 /* Retrieve generated string */
435 string didl = this.serializer.get_string ();
437 /* Set action return arguments */
438 this.action.set ("ObjectID", typeof (string), this.object.id,
439 "Result", typeof (string), didl);
441 this.action.return ();
445 private bool validate_create_class (WritableContainer container) {
446 var didl_cont = this.didl_object as DIDLLiteContainer;
447 var create_classes = didl_cont.get_create_classes ();
449 if (create_classes == null) {
453 foreach (var create_class in create_classes) {
454 if (!container.can_create (create_class)) {
462 private void handle_error (Error error) {
463 if (error is ContentDirectoryError) {
464 this.action.return_error (error.code, error.message);
466 this.action.return_error (701, error.message);
469 warning (_("Failed to create item under '%s': %s"),
476 private string get_generic_mime_type () {
477 if (!(this.object is MediaItem)) {
481 var item = this.object as MediaItem;
483 if (item is ImageItem) {
485 } else if (item is VideoItem) {
493 * Transfer information passed by caller to a MediaObject.
495 * WritableContainer works on MediaObject so we transfer the supplied data
496 * to one. Additionally some checks are performed (e.g. whether the DLNA
497 * profile is supported or not) or sanitize the supplied title for use as
498 * part of the on-disk filename.
500 * This function fills ObjectCreator.object.
502 private async void create_object_from_didl (WritableContainer container)
504 this.object = this.create_object (this.didl_object.id,
506 this.didl_object.title,
507 this.didl_object.upnp_class);
509 if (this.object is MediaItem) {
510 this.extract_item_parameters ();
513 // extract_item_parameters could not find an uri
514 if (this.object.uris.size == 0) {
515 var uri = yield this.create_uri (container, this.object.title);
516 this.object.uris.add (uri);
517 if (this.object is MediaItem) {
518 (this.object as MediaItem).place_holder = true;
521 if (this.object is MediaItem) {
522 var file = File.new_for_uri (this.object.uris[0]);
523 (this.object as MediaItem).place_holder = !file.is_native ();
527 this.object.id = this.object.uris[0];
529 this.parse_and_verify_didl_date ();
532 private void extract_item_parameters () throws Error {
533 var item = this.object as MediaItem;
535 var resources = this.didl_object.get_resources ();
536 if (resources != null && resources.length () > 0) {
537 var resource = resources.nth (0).data;
538 var info = resource.protocol_info;
541 if (info.dlna_profile != null) {
542 if (!this.is_profile_valid (info.dlna_profile)) {
543 var msg = _("DLNA profile '%s' not supported");
544 throw new ContentDirectoryError.BAD_METADATA
549 item.dlna_profile = info.dlna_profile;
552 if (info.mime_type != null) {
553 item.mime_type = info.mime_type;
557 string sanitized_uri = null;
558 if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
559 item.add_uri (sanitized_uri);
562 if (resource.size >= 0) {
563 item.size = resource.size;
567 if (item.mime_type == null) {
568 item.mime_type = this.get_generic_mime_type ();
576 private void parse_and_verify_didl_date () throws Error {
577 if (!(this.didl_object is DIDLLiteItem)) {
581 var didl_item = this.didl_object as DIDLLiteItem;
582 if (didl_item.date == null) {
586 var parsed_date = new Soup.Date.from_string (didl_item.date);
587 if (parsed_date != null) {
588 (this.object as MediaItem).date = parsed_date.to_string
589 (Soup.DateFormat.ISO8601);
594 int year = 0, month = 0, day = 0;
596 if (didl_item.date.scanf ("%4d-%02d-%02d",
600 throw new ContentDirectoryError.BAD_METADATA
601 (_("Invalid date format: %s"),
605 var date = GLib.Date ();
606 date.set_dmy ((DateDay) day, (DateMonth) month, (DateYear) year);
608 if (!date.valid ()) {
609 throw new ContentDirectoryError.BAD_METADATA
610 (_("Invalid date: %s"),
614 (this.object as MediaItem).date = didl_item.date + "T00:00:00";
617 private MediaObject create_object (string id,
618 WritableContainer parent,
622 switch (upnp_class) {
623 case ImageItem.UPNP_CLASS:
624 return new ImageItem (id, parent, title);
625 case PhotoItem.UPNP_CLASS:
626 return new PhotoItem (id, parent, title);
627 case VideoItem.UPNP_CLASS:
628 return new VideoItem (id, parent, title);
629 case AudioItem.UPNP_CLASS:
630 return new AudioItem (id, parent, title);
631 case MusicItem.UPNP_CLASS:
632 return new MusicItem (id, parent, title);
633 case PlaylistItem.UPNP_CLASS:
634 return new PlaylistItem (id, parent, title);
635 case MediaContainer.UPNP_CLASS:
636 case MediaContainer.STORAGE_FOLDER:
637 return new BaseMediaContainer (id, parent, title, 0);
638 case MediaContainer.PLAYLIST:
639 var container = new BaseMediaContainer (id, parent, title, 0);
640 container.upnp_class = upnp_class;
643 var msg = _("Cannot create object of class '%s': Not supported");
644 throw new ContentDirectoryError.BAD_METADATA (msg, upnp_class);
649 * Simple check for the validity of an URI.
651 * Check is done by parsing the URI with soup. Additionaly a cleaned-up
652 * version of the URI is returned in sanitized_uri.
654 * @param uri the input URI
655 * @param sanitized_uri containes a sanitized version of the URI on return
656 * @returns true if the URI is valid, false otherwise.
658 private bool is_valid_uri (string? uri, out string sanitized_uri) {
659 sanitized_uri = null;
660 if (uri == null || uri == "") {
664 var soup_uri = new Soup.URI (uri);
666 if (soup_uri == null || soup_uri.scheme == null) {
670 sanitized_uri = soup_uri.to_string (false);
676 * Transform the title to be usable on legacy file-systems such as FAT32.
678 * The function trims down the title to 205 chars (leaving room for an UUID)
679 * and replaces all special characters.
681 * @param title of the the media item
682 * @return the cleaned and shortened title
684 private string mangle_title (string title) throws Error {
685 var mangled = title.substring (0, int.min (title.length, 205));
686 mangled = this.title_regex.replace_literal (mangled,
690 RegexMatchFlags.NOTEMPTY);
692 return UUID.get () + "-" + mangled;
696 * Create an URI from the item's title.
698 * Create an unique URI from the supplied title by cleaning it from
699 * unwanted characters, shortening it and adding an UUID.
701 * @param container to create the item in
702 * @param title of the item to base the name on
703 * @returns an URI for the newly created item
705 private async string create_uri (WritableContainer container, string title)
707 var dir = yield container.get_writable (this.cancellable);
709 throw new ContentDirectoryError.RESTRICTED_PARENT
710 (_("Object creation in %s not allowed"),
714 var file = dir.get_child_for_display_name (this.mangle_title (title));
716 return file.get_uri ();
720 * Wait for the new object
722 * When creating an object in the back-end via WritableContainer.add_item
723 * or WritableContainer.add_container there might be a delay between the
724 * creation and the back-end having the newly created item available. This
725 * function waits for the item to become available by hooking into the
726 * container_updated signal. The maximum time to wait is 5 seconds.
728 * @param container to watch
730 private async void wait_for_object (WritableContainer container) {
731 debug ("Waiting for new object to appear under container '%s'…",
734 MediaObject object = null;
736 while (object == null) {
738 object = yield container.find_object (this.object.id,
740 } catch (Error error) {
741 var msg = _("Error from container '%s' on trying to find the newly added child object '%s' in it: %s");
742 warning (msg, container.id, this.object.id, error.message);
745 if (object == null) {
746 var id = container.container_updated.connect ((container) => {
747 this.wait_for_object.callback ();
751 timeout = Timeout.add_seconds (5, () => {
752 debug ("Timeout on waiting for 'updated' signal on '%s'.",
755 this.wait_for_object.callback ();
762 container.disconnect (id);
765 Source.remove (timeout);
771 debug ("Finished waiting for new object to appear under container '%s'",
774 this.object = object;
778 * Check if the profile is supported.
780 * The check is performed against the MediaEngine's database explicitly excluding
783 * @param profile to check
784 * @returns true if the profile is supported, false otherwise.
786 private bool is_profile_valid (string profile) {
787 unowned GLib.List<DLNAProfile> profiles, result;
789 var plugin = this.content_dir.root_device.resource_factory as MediaServerPlugin;
790 profiles = plugin.upload_profiles;
791 var p = new DLNAProfile (profile, "");
793 result = profiles.find_custom (p, DLNAProfile.compare_by_name);
795 return result != null;