chiark / gitweb /
Initial playlist tests and consequent fixes.
[disorder] / lib / trackdb-playlists.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2008 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file lib/trackdb-playlists.c
21  * @brief Track database playlist support
22  *
23  * This file implements reading and modification of playlists, including access
24  * control, but not locking or event logging (at least yet).
25  */
26 #include "common.h"
27
28 #include <errno.h>
29
30 #include "trackdb-int.h"
31 #include "mem.h"
32 #include "log.h"
33 #include "configuration.h"
34 #include "vector.h"
35
36 static int trackdb_playlist_get_tid(const char *name,
37                                     const char *who,
38                                     char ***tracksp,
39                                     int *ntracksp,
40                                     char **sharep,
41                                     DB_TXN *tid);
42 static int trackdb_playlist_set_tid(const char *name,
43                                     const char *who,
44                                     char **tracks,
45                                     int ntracks,
46                                     const char *share,
47                                     DB_TXN *tid);
48 static int trackdb_playlist_list_tid(const char *who,
49                                      char ***playlistsp,
50                                      int *nplaylistsp,
51                                      DB_TXN *tid);
52 static int trackdb_playlist_delete_tid(const char *name,
53                                        const char *who,
54                                        DB_TXN *tid);
55
56 /** @brief Parse a playlist name
57  * @param name Playlist name
58  * @param ownerp Where to put owner, or NULL
59  * @param sharep Where to put default sharing, or NULL
60  * @return 0 on success, -1 on error
61  *
62  * Playlists take the form USER.PLAYLIST or just PLAYLIST.  The PLAYLIST part
63  * is alphanumeric and nonempty.  USER is a username (see valid_username()).
64  */
65 int playlist_parse_name(const char *name,
66                         char **ownerp,
67                         char **sharep) {
68   const char *dot = strchr(name, '.'), *share;
69   char *owner;
70
71   if(dot) {
72     /* Owned playlist */
73     owner = xstrndup(name, dot - name);
74     if(!valid_username(owner))
75       return -1;
76     if(!valid_username(dot + 1))
77       return -1;
78     share = "private";
79   } else {
80     /* Shared playlist */
81     if(!valid_username(name))
82       return -1;
83     owner = 0;
84     share = "public";
85   }
86   if(ownerp)
87     *ownerp = owner;
88   if(sharep)
89     *sharep = xstrdup(share);
90   return 0;
91 }
92
93 /** @brief Check read access rights
94  * @param name Playlist name
95  * @param who Who wants to read
96  * @param share Playlist share status
97  */
98 static int playlist_may_read(const char *name,
99                              const char *who,
100                              const char *share) {
101   char *owner;
102   
103   if(playlist_parse_name(name, &owner, 0))
104     return 0;
105   /* Anyone can read shared playlists */
106   if(!owner)
107     return 1;
108   /* You can always read playlists you own */
109   if(!strcmp(owner, who))
110     return 1;
111   /* You can read public playlists */
112   if(!strcmp(share, "public"))
113     return 1;
114   /* Anything else is prohibited */
115   return 0;
116 }
117
118 /** @brief Check modify access rights
119  * @param name Playlist name
120  * @param who Who wants to modify
121  * @param share Playlist share status
122  */
123 static int playlist_may_write(const char *name,
124                               const char *who,
125                               const char attribute((unused)) *share) {
126   char *owner;
127   
128   if(playlist_parse_name(name, &owner, 0))
129     return 0;
130   /* Anyone can modify shared playlists */
131   if(!owner)
132     return 1;
133   /* You can always modify playlists you own */
134   if(!strcmp(owner, who))
135     return 1;
136   /* Anything else is prohibited */
137   return 0;
138 }
139
140 /** @brief Get playlist data
141  * @param name Name of playlist
142  * @param who Who wants to know
143  * @param tracksp Where to put list of tracks, or NULL
144  * @param ntracksp Where to put count of tracks, or NULL
145  * @param sharep Where to put sharing type, or NULL
146  * @return 0 on success, non-0 on error
147  *
148  * Possible return values:
149  * - @c 0 on success
150  * - @c ENOENT if the playlist doesn't exist
151  * - @c EINVAL if the playlist name is invalid
152  * - @c EACCES if the playlist cannot be read by @p who
153  */
154 int trackdb_playlist_get(const char *name,
155                          const char *who,
156                          char ***tracksp,
157                          int *ntracksp,
158                          char **sharep) {
159   int e;
160
161   if(playlist_parse_name(name, 0, 0)) {
162     error(0, "invalid playlist name '%s'", name);
163     return EINVAL;
164   }
165   WITH_TRANSACTION(trackdb_playlist_get_tid(name, who,
166                                             tracksp, ntracksp, sharep,
167                                             tid));
168   /* Don't expose libdb error codes too much */
169   if(e == DB_NOTFOUND)
170     e = ENOENT;
171   return e;
172 }
173
174 static int trackdb_playlist_get_tid(const char *name,
175                                     const char *who,
176                                     char ***tracksp,
177                                     int *ntracksp,
178                                     char **sharep,
179                                     DB_TXN *tid) {
180   struct kvp *k;
181   int e, ntracks;
182   const char *s;
183
184   if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
185     return e;
186   /* Get sharability */
187   if(!(s = kvp_get(k, "sharing"))) {
188     error(0, "playlist '%s' has no 'sharing' key", name);
189     s = "private";
190   }
191   /* Check the read is allowed */
192   if(!playlist_may_read(name, who, s))
193     return EACCES;
194   /* Return sharability */
195   if(sharep)
196     *sharep = xstrdup(s);
197   /* Get track count */
198   if(!(s = kvp_get(k, "count"))) {
199     error(0, "playlist '%s' has no 'count' key", name);
200     s = "0";
201   }
202   ntracks = atoi(s);
203   if(ntracks < 0) {
204     error(0, "playlist '%s' has negative count", name);
205     ntracks = 0;
206   }
207   /* Return track count */
208   if(ntracksp)
209     *ntracksp = ntracks;
210   if(tracksp) {
211     /* Get track list */
212     char **tracks = xcalloc(ntracks + 1, sizeof (char *));
213     char b[16];
214
215     for(int n = 0; n < ntracks; ++n) {
216       snprintf(b, sizeof b, "%d", n);
217       if(!(s = kvp_get(k, b))) {
218         error(0, "playlist '%s' lacks track %d", name, n);
219         s = "unknown";
220       }
221       tracks[n] = xstrdup(s);
222     }
223     tracks[ntracks] = 0;
224     /* Return track list */
225     *tracksp = tracks;
226   }
227   return 0;
228 }
229
230 /** @brief Modify or create a playlist
231  * @param name Playlist name
232  * @param tracks List of tracks to set, or NULL to leave alone
233  * @param ntracks Length of @p tracks
234  * @param share Sharing status, or NULL to leave alone
235  * @return 0 on success, non-0 on error
236  *
237  * If the playlist exists it is just modified.
238  *
239  * If the playlist does not exist it is created.  The default set of tracks is
240  * none, and the default sharing is private (if it is an owned one) or shared
241  * (otherwise).
242  *
243  * If neither @c tracks nor @c share are set then we only do an access check.
244  * The database is never modified (even to create the playlist) in this
245  * situation.
246  *
247  * Possible return values:
248  * - @c 0 on success
249  * - @c EINVAL if the playlist name is invalid
250  * - @c EACCES if the playlist cannot be modified by @p who
251  */
252 int trackdb_playlist_set(const char *name,
253                          const char *who,
254                          char **tracks,
255                          int ntracks,
256                          const char *share) {
257   int e;
258   char *owner;
259   
260   if(playlist_parse_name(name, &owner, 0)) {
261     error(0, "invalid playlist name '%s'", name);
262     return EINVAL;
263   }
264   /* Check valid share types */
265   if(share) {
266     if(owner) {
267       /* Playlists with an owner must be public or private */
268       if(strcmp(share, "public")
269          && strcmp(share, "private")) {
270         error(0, "playlist '%s' must be public or private", name);
271         return EINVAL;
272       }
273     } else {
274       /* Playlists with no owner must be shared */
275       if(strcmp(share, "shared")) {
276         error(0, "playlist '%s' must be shared", name);
277         return EINVAL;
278       }
279     }        
280   }
281   /* We've checked as much as we can for now, now go and attempt the change */
282   WITH_TRANSACTION(trackdb_playlist_set_tid(name, who, tracks, ntracks, share,
283                                             tid));
284   return e;
285 }
286
287 static int trackdb_playlist_set_tid(const char *name,
288                                     const char *who,
289                                     char **tracks,
290                                     int ntracks,
291                                     const char *share,
292                                     DB_TXN *tid) {
293   struct kvp *k;
294   int e;
295   const char *s;
296
297   if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid))
298      && e != DB_NOTFOUND)
299     return e;
300   /* If the playlist doesn't exist set some defaults */
301   if(e == DB_NOTFOUND) {
302     char *defshare, *owner;
303
304     if(playlist_parse_name(name, &owner, &defshare))
305       return EINVAL;
306     /* Can't create a non-shared playlist belonging to someone else.  In fact
307      * this should be picked up by playlist_may_write() below but it's clearer
308      * to do it here. */
309     if(owner && strcmp(owner, who))
310       return EACCES;
311     k = 0;
312     kvp_set(&k, "count", 0);
313     kvp_set(&k, "sharing", defshare);
314   }
315   /* Check that the modification is allowed */
316   if(!(s = kvp_get(k, "sharing"))) {
317     error(0, "playlist '%s' has no 'sharing' key", name);
318     s = "private";
319   }
320   if(!playlist_may_write(name, who, s))
321     return EACCES;
322   /* If no change was requested then don't even create */
323   if(!share && !tracks)
324     return 0;
325   /* Set the new values */
326   if(share)
327     kvp_set(&k, "sharing", share);
328   if(tracks) {
329     char b[16];
330     int oldcount, n;
331
332     /* Sanity check track count */
333     if(ntracks < 0 || ntracks > config->playlist_max) {
334       error(0, "invalid track count %d", ntracks);
335       return EINVAL;
336     }
337     /* Set the tracks */
338     for(n = 0; n < ntracks; ++n) {
339       snprintf(b, sizeof b, "%d", n);
340       kvp_set(&k, b, tracks[n]);
341     }
342     /* Get the old track count */
343     if((s = kvp_get(k, "count")))
344       oldcount = atoi(s);
345     else
346       oldcount = 0;
347     /* Delete old slots */
348     for(; n < oldcount; ++n) {
349       snprintf(b, sizeof b, "%d", n);
350       kvp_set(&k, b, NULL);
351     }
352     /* Set the new count */
353     snprintf(b, sizeof b, "%d", ntracks);
354     kvp_set(&k, "count", b);
355   }
356   /* Store the resulting record */
357   return trackdb_putdata(trackdb_playlistsdb, name, k, tid, 0);
358 }
359
360 /** @brief Get a list of playlists
361  * @param who Who wants to know
362  * @param playlistsp Where to put list of playlists
363  * @param nplaylistsp Where to put count of playlists, or NULL
364  */
365 void trackdb_playlist_list(const char *who,
366                            char ***playlistsp,
367                            int *nplaylistsp) {
368   int e;
369
370   WITH_TRANSACTION(trackdb_playlist_list_tid(who, playlistsp, nplaylistsp,
371                                              tid));
372 }
373
374 static int trackdb_playlist_list_tid(const char *who,
375                                      char ***playlistsp,
376                                      int *nplaylistsp,
377                                      DB_TXN *tid) {
378   struct vector v[1];
379   DBC *c;
380   DBT k[1], d[1];
381   int e;
382
383   vector_init(v);
384   c = trackdb_opencursor(trackdb_playlistsdb, tid);
385   memset(k, 0, sizeof k);
386   while(!(e = c->c_get(c, k, prepare_data(d), DB_NEXT))) {
387     char *name = xstrndup(k->data, k->size), *owner;
388     const char *share = kvp_get(kvp_urldecode(d->data, d->size),
389                                 "sharing");
390
391     /* Extract owner; malformed names are skipped */
392     if(playlist_parse_name(name, &owner, 0)) {
393       error(0, "invalid playlist name '%s' found in database", name);
394       continue;
395     }
396     if(!share) {
397       error(0, "playlist '%s' has no 'sharing' key", name);
398       continue;
399     }
400     /* Always list public and shared playlists
401      * Only list private ones to their owner
402      * Don't list anything else
403      */
404     if(!strcmp(share, "public")
405        || !strcmp(share, "shared")
406        || (!strcmp(share, "private")
407            && owner && !strcmp(owner, who)))
408       vector_append(v, name);
409   }
410   trackdb_closecursor(c);
411   switch(e) {
412   case DB_NOTFOUND:
413     break;
414   case DB_LOCK_DEADLOCK:
415     return e;
416   default:
417     fatal(0, "c->c_get: %s", db_strerror(e));
418   }
419   vector_terminate(v);
420   if(playlistsp)
421     *playlistsp = v->vec;
422   if(nplaylistsp)
423     *nplaylistsp = v->nvec;
424   return 0;
425 }
426
427 /** @brief Delete a playlist
428  * @param name Playlist name
429  * @param who Who is deleting it
430  * @return 0 on success, non-0 on error
431  *
432  * Possible return values:
433  * - @c 0 on success
434  * - @c EINVAL if the playlist name is invalid
435  * - @c EACCES if the playlist cannot be modified by @p who
436  * - @c ENOENT if the playlist doesn't exist
437  */
438 int trackdb_playlist_delete(const char *name,
439                             const char *who) {
440   int e;
441   char *owner;
442   
443   if(playlist_parse_name(name, &owner, 0)) {
444     error(0, "invalid playlist name '%s'", name);
445     return EINVAL;
446   }
447   /* We've checked as much as we can for now, now go and attempt the change */
448   WITH_TRANSACTION(trackdb_playlist_delete_tid(name, who, tid));
449   if(e == DB_NOTFOUND)
450     e = ENOENT;
451   return e;
452 }
453
454 static int trackdb_playlist_delete_tid(const char *name,
455                                        const char *who,
456                                        DB_TXN *tid) {
457   struct kvp *k;
458   int e;
459   const char *s;
460
461   if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
462     return e;
463   /* Check that modification is allowed */
464   if(!(s = kvp_get(k, "sharing"))) {
465     error(0, "playlist '%s' has no 'sharing' key", name);
466     s = "private";
467   }
468   if(!playlist_may_write(name, who, s))
469     return EACCES;
470   /* Delete the playlist */
471   return trackdb_delkey(trackdb_playlistsdb, name, tid);
472 }
473
474 /*
475 Local Variables:
476 c-basic-offset:2
477 comment-column:40
478 fill-column:79
479 indent-tabs-mode:nil
480 End:
481 */