2 * Copyright (C) 2012 Nokia Corporation.
4 * Author: Jens Georg <jensg@openismus.com>
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.
23 [CCode (cname = "uuid_generate", cheader_filename = "uuid/uuid.h")]
24 internal extern static void uuid_generate ([CCode (array_length = false)]
26 [CCode (cname = "uuid_unparse", cheader_filename = "uuid/uuid.h")]
27 internal extern static void uuid_unparse ([CCode (array_length = false)]
29 [CCode (array_length = false)]
32 public const string DIDL_ITEM = """<?xml version="1.0" encoding="UTF-8"?>
34 xmlns:dc="http://purl.org/dc/elements/1.1/"
35 xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
36 xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
37 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
38 xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/
39 http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd
40 urn:schemas-upnp-org:metadata-1-0/upnp/
41 http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd">
42 <item id="" parentID="0" restricted="0">
43 <dc:title>New Song</dc:title>
44 <upnp:class>object.item.audioItem</upnp:class>
45 <res protocolInfo="*:*:*:*" />
49 public class Rygel.ServiceAction : GLib.Object {
50 public int error_code;
51 public string error_message;
53 public string elements;
55 public ServiceAction (string? container_id,
57 this.id = container_id;
58 this.elements = elements;
61 public void @return() {}
62 public void return_error (int code, string message) {
63 this.error_code = code;
64 this.error_message = message;
67 public new void @get (string arg1_name,
72 out string arg2_val) {
73 assert (arg1_name == "ContainerID");
74 assert (arg1_type == typeof (string));
77 assert (arg2_name == "Elements");
78 assert (arg2_type == typeof (string));
82 public new void @set (string arg1_name,
88 assert (arg1_name == "ObjectID");
89 assert (arg1_type == typeof (string));
91 assert (arg2_name == "Result");
92 assert (arg2_type == typeof (string));
96 public class Rygel.HTTPServer : GLib.Object {
99 public class Rygel.ItemRemovalQueue : GLib.Object {
100 public static ItemRemovalQueue get_default () {
101 return new ItemRemovalQueue ();
104 public void queue (MediaItem item, Cancellable? cancellable) {
108 public class Rygel.MediaObject : GLib.Object {
110 public string ref_id;
111 public unowned MediaContainer parent;
112 public string upnp_class;
114 public GUPnP.OCMFlags ocm_flags;
115 public Gee.ArrayList<string> uris;
117 public void add_uri (string uri) {
122 public class Rygel.MediaItem : Rygel.MediaObject {
123 public string dlna_profile;
124 public string mime_type;
126 public bool place_holder;
129 public MediaItem (string id, MediaContainer parent, string title) {
131 this.parent = parent;
135 public void serialize (GUPnP.DIDLLiteWriter writer, HTTPServer server) {
139 public class Rygel.MusicItem : Rygel.AudioItem {
140 public const string UPNP_CLASS = "object.item.audioItem.musicTrack";
142 public MusicItem (string id, MediaContainer parent, string title) {
143 base (id, parent, title);
147 public class Rygel.AudioItem : Rygel.MediaItem {
148 public const string UPNP_CLASS = "object.item.audioItem";
149 public string artist;
152 public AudioItem (string id, MediaContainer parent, string title) {
153 base (id, parent, title);
156 public class Rygel.ImageItem : Rygel.MediaItem {
157 public const string UPNP_CLASS = "object.item.imageItem";
158 public ImageItem (string id, MediaContainer parent, string title) {
159 base (id, parent, title);
163 public class Rygel.VideoItem : Rygel.MediaItem {
164 public const string UPNP_CLASS = "object.item.videoItem";
165 public VideoItem (string id, MediaContainer parent, string title) {
166 base (id, parent, title);
170 public class Rygel.PhotoItem : Rygel.MediaItem {
171 public const string UPNP_CLASS = "object.item.imageItem.photo";
172 public string creator;
174 public PhotoItem (string id, MediaContainer parent, string title) {
175 base (id, parent, title);
178 public class Rygel.ContentDirectory : GLib.Object {
179 public Cancellable cancellable;
180 public MediaContainer root_container;
181 public HTTPServer http_server;
184 public class Rygel.MediaContainer : Rygel.MediaObject {
185 public Gee.ArrayList<string> create_classes = new Gee.ArrayList<string> ();
186 public int child_count;
187 public string sort_criteria = "+dc:title";
190 public MediaObject found_object = null;
192 public async MediaObject? find_object (string id,
193 Cancellable? cancellable = null) {
194 Idle.add (() => { find_object.callback (); return false; });
200 public signal void container_updated (MediaContainer container);
203 public class Rygel.MediaObjects : Gee.ArrayList<MediaObject> {
206 public class Rygel.WritableContainer : Rygel.MediaContainer {
207 public bool can_create (string upnp_class) {
208 return this.create_classes.contains (upnp_class);
211 public async File? get_writable (Cancellable? cancellable = null) {
212 return File.new_for_commandline_arg ("/tmp");
215 public async void add_item (MediaItem item,
216 Cancellable? cancellable = null) {
220 public class Rygel.SearchableContainer : Rygel.MediaContainer {
221 public MediaObjects result = new MediaObjects ();
223 public async MediaObjects search (SearchExpression expression,
226 out int total_matches,
227 string soer_criteria,
228 Cancellable? cancellable = null) {
229 Idle.add (() => { search.callback (); return false; });
236 public errordomain Rygel.ContentDirectoryError {
244 public class Rygel.HTTPItemCreatorTest : GLib.Object {
246 public static int main (string[] args) {
247 var test = new HTTPItemCreatorTest ();
248 test.test_parse_args ();
249 test.test_didl_parsing ();
250 test.test_fetch_container ();
256 Error no_such_object;
257 Error restricted_parent;
261 public HTTPItemCreatorTest () {
262 this.no_such_object = new ContentDirectoryError.NO_SUCH_OBJECT("");
263 this.restricted_parent = new ContentDirectoryError.RESTRICTED_PARENT("");
264 this.bad_metadata = new ContentDirectoryError.BAD_METADATA("");
265 this.invalid_args = new ContentDirectoryError.INVALID_ARGS("");
268 private void test_parse_args () {
269 // check null container id
270 var content_directory = new ContentDirectory ();
272 var action = new ServiceAction (null, "");
273 var creator = new ItemCreator (content_directory, action);
275 assert (action.error_code == no_such_object.code);
277 // check elements containing a comment
278 action = new ServiceAction ("0", "<!-- This is an XML comment -->");
279 creator = new ItemCreator (content_directory, action);
281 assert (action.error_code == bad_metadata.code);
283 // check null elements
284 action = new ServiceAction ("0", null);
285 creator = new ItemCreator (content_directory, action);
287 assert (action.error_code == bad_metadata.code);
290 private void test_didl_parsing_step (Xml.Doc *doc, int expected_code) {
293 doc->dump_memory_enc (out xml);
294 var action = new ServiceAction ("0", xml);
295 var content_directory = new ContentDirectory ();
296 var creator = new ItemCreator (content_directory, action);
298 assert (action.error_code == expected_code);
301 private void test_didl_parsing () {
302 var xml = Xml.Parser.read_memory (DIDL_ITEM,
306 Xml.ParserOption.RECOVER |
307 Xml.ParserOption.NOBLANKS);
308 var didl_node = xml->children;
309 var item_node = didl_node->children;
310 var content_directory = new ContentDirectory ();
313 var action = new ServiceAction ("0", "");
314 var creator = new ItemCreator (content_directory, action);
316 assert (action.error_code == bad_metadata.code);
317 assert (action.error_message == "Bad metadata");
320 item_node->unlink ();
321 didl_node->set_content (" ");
322 this.test_didl_parsing_step (xml, bad_metadata.code);
324 // test item node with missing restricted attribute
325 var tmp = item_node->copy (1);
326 tmp->unset_prop ("restricted");
327 didl_node->add_child (tmp);
328 this.test_didl_parsing_step (xml, bad_metadata.code);
330 // test item node with restricted=1
331 tmp->set_prop ("restricted", "1");
332 this.test_didl_parsing_step (xml, invalid_args.code);
334 // test item node with invalid id
336 tmp = item_node->copy (1);
337 tmp->set_prop ("id", "InvalidItemId");
338 didl_node->add_child (tmp);
339 this.test_didl_parsing_step (xml, bad_metadata.code);
341 // test item node with missing id
342 tmp->unset_prop ("id");
343 this.test_didl_parsing_step (xml, bad_metadata.code);
345 // test item node with missing title
347 tmp = item_node->copy (1);
348 var title_node = tmp->children;
349 title_node->unlink ();
350 didl_node->add_child (tmp);
351 this.test_didl_parsing_step (xml, bad_metadata.code);
353 // test missing, empty or non-item upnp class
355 tmp = item_node->copy (1);
356 var class_node = tmp->children->next;
357 class_node->set_content ("object.container");
358 didl_node->add_child (tmp);
359 this.test_didl_parsing_step (xml, bad_metadata.code);
361 class_node->set_content ("");
362 this.test_didl_parsing_step (xml, bad_metadata.code);
364 class_node->unlink ();
365 this.test_didl_parsing_step (xml, bad_metadata.code);
368 private void test_fetch_container_run (ItemCreator creator) {
369 var main_loop = new MainLoop (null, false);
370 creator.run.begin ( () => { main_loop.quit (); });
374 private void test_fetch_container () {
375 // check case when object is not found
376 var content_directory = new ContentDirectory ();
377 var root_container = new SearchableContainer ();
378 content_directory.root_container = root_container;
379 var action = new ServiceAction ("0", DIDL_ITEM);
380 var creator = new ItemCreator (content_directory, action);
381 this.test_fetch_container_run (creator);
382 assert (action.error_code == no_such_object.code);
384 // check case when found object is not a container → Error 710
385 // cf. ContentDirectory:2 spec, Table 2-22
386 root_container.found_object = new MediaObject ();
387 this.test_fetch_container_run (creator);
388 assert (action.error_code == no_such_object.code);
390 // check case when found container does not have OCMUpload set
391 root_container.found_object = new MediaContainer ();
392 this.test_fetch_container_run (creator);
393 assert (action.error_code == restricted_parent.code);
395 // check case when found container is not a writable container
396 root_container.found_object.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
397 this.test_fetch_container_run (creator);
398 assert (action.error_code == restricted_parent.code);
400 // check when found container does not have the correct create class
401 var container = new WritableContainer ();
402 container.create_classes.add ("object.item.imageItem.musicTrack");
403 container.ocm_flags |= GUPnP.OCMFlags.UPLOAD;
404 root_container.found_object = container;
405 this.test_fetch_container_run (creator);
406 assert (action.error_code == bad_metadata.code);
408 // check DLNA.ORG_AnyContainer when root container is not searchable
409 content_directory.root_container = new MediaContainer ();
410 action.id = "DLNA.ORG_AnyContainer";
411 this.test_fetch_container_run (creator);
412 assert (action.error_code == no_such_object.code);
414 // check DLNA.ORG_AnyContainer when no writable container is found
415 content_directory.root_container = new SearchableContainer ();
416 this.test_fetch_container_run (creator);
417 // We cannot distinguish this case from the "create-class doesn't match"
419 assert (action.error_code == bad_metadata.code);