3e44e463d6a91911733e92006f6d3cd72a5a2d7a
[profile/ivi/rygel.git] / src / librygel-server / rygel-object-creator.vala
1 /*
2  * Copyright (C) 2010-2011 Nokia Corporation.
3  * Copyright (C) 2012 Intel Corporation.
4  *
5  * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
6  *         Jens Georg <jensg@openismus.com>
7  *
8  * This file is part of Rygel.
9  *
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.
14  *
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.
19  *
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.
23  */
24
25 using GUPnP;
26
27 /**
28  * Dummy implementation of Rygel.MediaContainer to pass on to
29  * Rygel.WritableContianer for creation.
30  */
31 private class Rygel.BaseMediaContainer : MediaContainer {
32     /**
33      * Create a media container with the specified details.
34      *
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.
39      */
40     public BaseMediaContainer (string          id,
41                                MediaContainer? parent,
42                                string          title,
43                                int             child_count) {
44         Object (id : id,
45                 parent : parent,
46                 title : title,
47                 child_count : child_count);
48     }
49
50     /**
51      * Fetches the list of media objects directly under this container.
52      *
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
57      *
58      * @return A list of media objects.
59      */
60     public override async MediaObjects? get_children
61                                             (uint         offset,
62                                              uint         max_count,
63                                              string       sort_criteria,
64                                              Cancellable? cancellable)
65                                             throws Error {
66         return null;
67     }
68
69     /**
70      * Recursively searches this container for a media object with the given ID.
71      *
72      * @param id ID of the media object to search for
73      * @param cancellable optional cancellable for this operation
74      *
75      * @return the found media object.
76      */
77     public override async MediaObject? find_object (string       id,
78                                                     Cancellable? cancellable)
79                                                     throws Error {
80         return null;
81     }
82
83 }
84
85
86
87 /**
88  * CreateObject action implementation.
89  */
90 internal class Rygel.ObjectCreator: GLib.Object, Rygel.StateMachine {
91     private static PatternSpec comment_pattern = new PatternSpec ("*<!--*-->*");
92
93     private const string INVALID_CHARS = "/?<>\\:*|\"";
94
95     // In arguments
96     private string container_id;
97     private string elements;
98
99     private DIDLLiteObject didl_object;
100     private MediaObject object;
101
102     private ContentDirectory content_dir;
103     private ServiceAction action;
104     private Serializer serializer;
105     private DIDLLiteParser didl_parser;
106     private Regex title_regex;
107
108     public Cancellable cancellable { get; set; }
109
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 ();
117         try {
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 (); }
123     }
124
125     public async void run () {
126         try {
127             this.parse_args ();
128             this.parse_didl ();
129
130             var container = yield this.fetch_container ();
131
132             /* Verify the create class. Note that we always assume
133              * createClass@includeDerived to be false.
134              *
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.
138              */
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,
145                                          container.id);
146             }
147
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"));
152             }
153
154             yield this.create_object_from_didl (container);
155             if (this.object is MediaItem) {
156                 yield container.add_item (this.object as MediaItem,
157                                           this.cancellable);
158             } else {
159                 yield container.add_container (this.object as MediaContainer,
160                                                this.cancellable);
161             }
162
163             yield this.wait_for_object (container);
164
165             this.object.serialize (serializer, this.content_dir.http_server);
166
167             // Conclude the successful action
168             this.conclude ();
169
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 ();
174
175                 queue.queue (this.object, this.cancellable);
176             }
177         } catch (Error err) {
178             this.handle_error (err);
179         }
180     }
181
182     /**
183      * Check the supplied input parameters.
184      */
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);
189
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"));
196         }
197
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"));
202         }
203     }
204
205     /**
206      * Parse the given DIDL-Lite snippet.
207      *
208      * Parses the DIDL-Lite and performs checking of the passed meta-data
209      * according to UPnP and DLNA guidelines.
210      */
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;
216         });
217
218         try {
219             this.didl_parser.parse_didl (this.elements);
220         } catch (Error parse_err) {
221             throw new ContentDirectoryError.BAD_METADATA ("Bad metadata");
222         }
223
224         if (this.didl_object == null) {
225             var message = _("No objects in DIDL-Lite from client: '%s'");
226
227             throw new ContentDirectoryError.BAD_METADATA
228                                         (message, this.elements);
229         }
230
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);
234         }
235
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);
239         }
240
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 &
245              (OCMFlags.UPLOAD |
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);
250         }
251
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"));
257         }
258
259         if (didl_object.restricted) {
260             throw new ContentDirectoryError.BAD_METADATA
261                                         (_("Cannot create restricted item"));
262         }
263
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;
270             }
271         }
272     }
273
274     /**
275      * Modify the give UPnP class to be a more general one.
276      *
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
280      *
281      * @param upnp_class the current UPnP class which will be modified in-place.
282      */
283     private void generalize_upnp_class (ref string upnp_class) {
284         char *needle = upnp_class.rstr_len (-1, ".");
285         if (needle != null) {
286             *needle = '\0';
287         }
288     }
289
290     private async SearchExpression build_create_class_expression
291                                         (SearchExpression expression) {
292         // Take create-classes into account
293         if (!(this.didl_object is DIDLLiteContainer)) {
294             return expression;
295         }
296
297         var didl_container = this.didl_object as DIDLLiteContainer;
298         var create_classes = didl_container.get_create_classes ();
299         if (create_classes == null) {
300             return expression;
301         }
302
303         var builder = new StringBuilder ("(");
304         foreach (var create_class in create_classes) {
305             builder.append_printf ("(upnp:createClass derivedfrom \"%s\") AND",
306                                    create_class);
307         }
308
309         // remove dangeling AND
310         builder.truncate (builder.len - 3);
311         builder.append (")");
312
313         try {
314             var parser = new Rygel.SearchCriteriaParser (builder.str);
315             yield parser.run ();
316
317             var rel = new LogicalExpression ();
318             rel.operand1 = expression;
319             rel.op = LogicalOperator.AND;
320             rel.operand2 = parser.expression;
321
322             return rel;
323         } catch (Error error) {
324             assert_not_reached ();
325         }
326     }
327
328     /**
329      * Find a container that can create items matching the UPnP class of the
330      * requested item.
331      *
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.
334      *
335      * @returns a container able to create the item or null if no such container
336      *          can be found.
337      */
338     private async MediaObject? find_any_container () throws Error {
339         var root_container = this.content_dir.root_container
340                                         as SearchableContainer;
341
342         if (root_container == null) {
343             return null;
344         }
345
346         var upnp_class = this.didl_object.upnp_class;
347
348         var expression = new RelationalExpression ();
349         expression.op = SearchCriteriaOp.DERIVED_FROM;
350         expression.operand1 = "upnp:createClass";
351
352         // Add container's create classes to the search expression if there
353         // are some
354         var search_expression = yield this.build_create_class_expression
355                                         (expression);
356
357         while (upnp_class != "object") {
358             expression.operand2 = upnp_class;
359
360             uint total_matches;
361             var result = yield root_container.search (search_expression,
362                                                       0,
363                                                       1,
364                                                       out total_matches,
365                                                       root_container.sort_criteria,
366                                                       this.cancellable);
367             if (result.size > 0) {
368                 this.didl_object.upnp_class = upnp_class;
369
370                 return result[0];
371             } else {
372                 this.generalize_upnp_class (ref upnp_class);
373             }
374         }
375
376         if (upnp_class == "object") {
377             throw new ContentDirectoryError.BAD_METADATA
378                                     (_("UPnP class '%s' not supported"),
379                                      this.didl_object.upnp_class);
380         }
381
382         return null;
383     }
384
385     /**
386      * Get the container to create the item in.
387      *
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"
390      * id.
391      *
392      * @return an instance of WritableContainer matching the criteria
393      * @throws ContentDirectoryError for various problems
394      */
395     private async WritableContainer fetch_container () throws Error {
396         MediaObject media_object = null;
397
398         if (this.container_id == MediaContainer.ANY) {
399             media_object = yield this.find_any_container ();
400         } else {
401             media_object = yield this.content_dir.root_container.find_object
402                                         (this.container_id, this.cancellable);
403         }
404
405         if (media_object == null || !(media_object is MediaContainer)) {
406             throw new ContentDirectoryError.NO_SUCH_CONTAINER
407                                         (_("No such container"));
408         }
409
410         if (!(media_object is WritableContainer)) {
411             throw new ContentDirectoryError.RESTRICTED_PARENT
412                                         (_(" %%% Object creation in %s not allowed"),
413                                          media_object.id);
414         }
415
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"),
425                                          media_object.id);
426         }
427
428         // FIXME: Check for @restricted=1 missing?
429
430         return media_object as WritableContainer;
431     }
432
433     private void conclude () {
434         /* Retrieve generated string */
435         string didl = this.serializer.get_string ();
436
437         /* Set action return arguments */
438         this.action.set ("ObjectID", typeof (string), this.object.id,
439                          "Result", typeof (string), didl);
440
441         this.action.return ();
442         this.completed ();
443     }
444
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 ();
448
449         if (create_classes == null) {
450             return true;
451         }
452
453         foreach (var create_class in create_classes) {
454             if (!container.can_create (create_class)) {
455                 return false;
456             }
457         }
458
459         return true;
460     }
461
462     private void handle_error (Error error) {
463         if (error is ContentDirectoryError) {
464             this.action.return_error (error.code, error.message);
465         } else {
466             this.action.return_error (701, error.message);
467         }
468
469         warning (_("Failed to create item under '%s': %s"),
470                  this.container_id,
471                  error.message);
472
473         this.completed ();
474     }
475
476     private string get_generic_mime_type () {
477         if (!(this.object is MediaItem)) {
478             return "";
479         }
480
481         var item = this.object as MediaItem;
482
483         if (item is ImageItem) {
484             return "image";
485         } else if (item is VideoItem) {
486             return "video";
487         } else {
488             return "audio";
489         }
490     }
491
492     /**
493      * Transfer information passed by caller to a MediaObject.
494      *
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.
499      *
500      * This function fills ObjectCreator.object.
501      */
502     private async void create_object_from_didl (WritableContainer container)
503                                                 throws Error {
504         this.object = this.create_object (this.didl_object.id,
505                                           container,
506                                           this.didl_object.title,
507                                           this.didl_object.upnp_class);
508
509         if (this.object is MediaItem) {
510             this.extract_item_parameters ();
511         }
512
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;
519             }
520         } else {
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 ();
524             }
525         }
526
527         this.object.id = this.object.uris[0];
528
529         this.parse_and_verify_didl_date ();
530     }
531
532     private void extract_item_parameters () throws Error {
533         var item = this.object as MediaItem;
534
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;
539
540             if (info != null) {
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
545                                     (msg,
546                                      info.dlna_profile);
547                     }
548
549                     item.dlna_profile = info.dlna_profile;
550                 }
551
552                 if (info.mime_type != null) {
553                     item.mime_type = info.mime_type;
554                 }
555             }
556
557             string sanitized_uri = null;
558             if (this.is_valid_uri (resource.uri, out sanitized_uri)) {
559                 item.add_uri (sanitized_uri);
560             }
561
562             if (resource.size >= 0) {
563                 item.size = resource.size;
564             }
565         }
566
567         if (item.mime_type == null) {
568             item.mime_type = this.get_generic_mime_type ();
569         }
570
571         if (item.size < 0) {
572             item.size = 0;
573         }
574     }
575
576     private void parse_and_verify_didl_date () throws Error {
577         if (!(this.didl_object is DIDLLiteItem)) {
578             return;
579         }
580
581         var didl_item = this.didl_object as DIDLLiteItem;
582         if (didl_item.date == null) {
583             return;
584         }
585
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);
590
591             return;
592         }
593
594         int year = 0, month = 0, day = 0;
595
596         if (didl_item.date.scanf ("%4d-%02d-%02d",
597                                   out year,
598                                   out month,
599                                   out day) != 3) {
600             throw new ContentDirectoryError.BAD_METADATA
601                                     (_("Invalid date format: %s"),
602                                      didl_item.date);
603         }
604
605         var date = GLib.Date ();
606         date.set_dmy ((DateDay) day, (DateMonth) month, (DateYear) year);
607
608         if (!date.valid ()) {
609             throw new ContentDirectoryError.BAD_METADATA
610                                     (_("Invalid date: %s"),
611                                      didl_item.date);
612         }
613
614         (this.object as MediaItem).date = didl_item.date + "T00:00:00";
615     }
616
617     private MediaObject create_object (string            id,
618                                        WritableContainer parent,
619                                        string            title,
620                                        string            upnp_class)
621                                        throws Error {
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;
641             return container;
642         default:
643             var msg = _("Cannot create object of class '%s': Not supported");
644             throw new ContentDirectoryError.BAD_METADATA (msg, upnp_class);
645         }
646     }
647
648     /**
649      * Simple check for the validity of an URI.
650      *
651      * Check is done by parsing the URI with soup. Additionaly a cleaned-up
652      * version of the URI is returned in sanitized_uri.
653      *
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.
657      */
658     private bool is_valid_uri (string? uri, out string sanitized_uri) {
659         sanitized_uri = null;
660         if (uri == null || uri == "") {
661             return false;
662         }
663
664         var soup_uri = new Soup.URI (uri);
665
666         if (soup_uri == null || soup_uri.scheme == null) {
667             return false;
668         }
669
670         sanitized_uri = soup_uri.to_string (false);
671
672         return true;
673     }
674
675     /**
676      * Transform the title to be usable on legacy file-systems such as FAT32.
677      *
678      * The function trims down the title to 205 chars (leaving room for an UUID)
679      * and replaces all special characters.
680      *
681      * @param title of the the media item
682      * @return the cleaned and shortened title
683      */
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,
687                                                     -1,
688                                                     0,
689                                                     "_",
690                                                     RegexMatchFlags.NOTEMPTY);
691
692         return UUID.get () + "-" + mangled;
693     }
694
695     /**
696      * Create an URI from the item's title.
697      *
698      * Create an unique URI from the supplied title by cleaning it from
699      * unwanted characters, shortening it and adding an UUID.
700      *
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
704      */
705     private async string create_uri (WritableContainer container, string title)
706                                     throws Error {
707         var dir = yield container.get_writable (this.cancellable);
708         if (dir == null) {
709             throw new ContentDirectoryError.RESTRICTED_PARENT
710                                         (_("Object creation in %s not allowed"),
711                                          container.id);
712         }
713
714         var file = dir.get_child_for_display_name (this.mangle_title (title));
715
716         return file.get_uri ();
717     }
718
719     /**
720      * Wait for the new object
721      *
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.
727      *
728      * @param container to watch
729      */
730     private async void wait_for_object (WritableContainer container) {
731         debug ("Waiting for new object to appear under container '%s'…",
732                container.id);
733
734         MediaObject object = null;
735
736         while (object == null) {
737             try {
738                 object = yield container.find_object (this.object.id,
739                                                       this.cancellable);
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);
743             }
744
745             if (object == null) {
746                 var id = container.container_updated.connect ((container) => {
747                     this.wait_for_object.callback ();
748                 });
749
750                 uint timeout = 0;
751                 timeout = Timeout.add_seconds (5, () => {
752                     debug ("Timeout on waiting for 'updated' signal on '%s'.",
753                            container.id);
754                     timeout = 0;
755                     this.wait_for_object.callback ();
756
757                     return false;
758                 });
759
760                 yield;
761
762                 container.disconnect (id);
763
764                 if (timeout != 0) {
765                     Source.remove (timeout);
766                 } else {
767                     break;
768                 }
769             }
770         }
771         debug ("Finished waiting for new object to appear under container '%s'",
772                container.id);
773
774         this.object = object;
775     }
776
777     /**
778      * Check if the profile is supported.
779      *
780      * The check is performed against the MediaEngine's database explicitly excluding
781      * the transcoders.
782      *
783      * @param profile to check
784      * @returns true if the profile is supported, false otherwise.
785      */
786     private bool is_profile_valid (string profile) {
787         unowned GLib.List<DLNAProfile> profiles, result;
788
789         var plugin = this.content_dir.root_device.resource_factory as MediaServerPlugin;
790         profiles = plugin.upload_profiles;
791         var p = new DLNAProfile (profile, "");
792
793         result = profiles.find_custom (p, DLNAProfile.compare_by_name);
794
795         return result != null;
796     }
797 }