1<?php
2/*
3 * vim:set softtabstop=4 shiftwidth=4 expandtab:
4 *
5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
6 * Copyright 2001 - 2020 Ampache.org
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program 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 Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 *
21 */
22
23declare(strict_types=0);
24
25namespace Ampache\Module\Api;
26
27use Ampache\Config\AmpConfig;
28use Ampache\Repository\Model\Catalog;
29use Ampache\Module\Authorization\Access;
30use Ampache\Repository\Model\Browse;
31use Ampache\Module\System\Dba;
32use Ampache\Repository\Model\Preference;
33use Ampache\Repository\UserRepositoryInterface;
34
35/**
36 * API Class
37 *
38 * This handles functions relating to the API written for Ampache, initially
39 * this is very focused on providing functionality for Amarok so it can
40 * integrate with Ampache.
41 */
42class Api
43{
44    /**
45     * This dict contains all known api-methods (key) and their respective handler (value)
46     */
47    public const METHOD_LIST = [
48        'handshake' => Method\HandshakeMethod::class,
49        'ping' => Method\PingMethod::class,
50        'goodbye' => Method\GoodbyeMethod::class,
51        'url_to_song' => Method\UrlToSongMethod::class,
52        'get_indexes' => Method\GetIndexesMethod::class,
53        'get_bookmark' => Method\GetBookmarkMethod::class,
54        'get_similar' => Method\GetSimilarMethod::class,
55        'advanced_search' => Method\AdvancedSearchMethod::class,
56        'artists' => Method\ArtistsMethod::class,
57        'artist' => Method\ArtistMethod::class,
58        'artist_albums' => Method\ArtistAlbumsMethod::class,
59        'artist_songs' => Method\ArtistSongsMethod::class,
60        Method\AlbumsMethod::ACTION => Method\AlbumsMethod::class,
61        Method\AlbumMethod::ACTION => Method\AlbumMethod::class,
62        'album_songs' => Method\AlbumSongsMethod::class,
63        'licenses' => Method\LicensesMethod::class,
64        'license' => Method\LicenseMethod::class,
65        'license_songs' => Method\LicenseSongsMethod::class,
66        'tags' => Method\TagsMethod::class,
67        'tag' => Method\TagMethod::class,
68        'tag_artists' => Method\TagArtistsMethod::class,
69        'tag_albums' => Method\TagAlbumsMethod::class,
70        'tag_songs' => Method\TagSongsMethod::class,
71        'genres' => Method\GenresMethod::class,
72        'genre' => Method\GenreMethod::class,
73        'genre_artists' => Method\GenreArtistsMethod::class,
74        'genre_albums' => Method\GenreAlbumsMethod::class,
75        'genre_songs' => Method\GenreSongsMethod::class,
76        'labels' => Method\LabelsMethod::class,
77        'label' => Method\LabelMethod::class,
78        'label_artists' => Method\LabelArtistsMethod::class,
79        'songs' => Method\SongsMethod::class,
80        'song' => Method\SongMethod::class,
81        'song_delete' => Method\SongDeleteMethod::class,
82        'playlists' => Method\PlaylistsMethod::class,
83        'playlist' => Method\PlaylistMethod::class,
84        'playlist_songs' => Method\PlaylistSongsMethod::class,
85        'playlist_create' => Method\PlaylistCreateMethod::class,
86        'playlist_edit' => Method\PlaylistEditMethod::class,
87        'playlist_delete' => Method\PlaylistDeleteMethod::class,
88        'playlist_add_song' => Method\PlaylistAddSongMethod::class,
89        'playlist_remove_song' => Method\PlaylistRemoveSongMethod::class,
90        'playlist_generate' => Method\PlaylistGenerateMethod::class,
91        'search_songs' => Method\SearchSongsMethod::class,
92        'shares' => Method\SharesMethod::class,
93        'share' => Method\ShareMethod::class,
94        'share_create' => Method\ShareCreateMethod::class,
95        'share_delete' => Method\ShareDeleteMethod::class,
96        'share_edit' => Method\ShareEditMethod::class,
97        'bookmarks' => Method\BookmarksMethod::class,
98        'bookmark_create' => Method\BookmarkCreateMethod::class,
99        'bookmark_edit' => Method\BookmarkEditMethod::class,
100        'bookmark_delete' => Method\BookmarkDeleteMethod::class,
101        'videos' => Method\VideosMethod::class,
102        'video' => Method\VideoMethod::class,
103        'stats' => Method\StatsMethod::class,
104        'podcasts' => Method\PodcastsMethod::class,
105        'podcast' => Method\PodcastMethod::class,
106        'podcast_create' => Method\PodcastCreateMethod::class,
107        'podcast_delete' => Method\PodcastDeleteMethod::class,
108        'podcast_edit' => Method\PodcastEditMethod::class,
109        'podcast_episodes' => Method\PodcastEpisodesMethod::class,
110        'podcast_episode' => Method\PodcastEpisodeMethod::class,
111        'podcast_episode_delete' => Method\PodcastEpisodeDeleteMethod::class,
112        'users' => Method\UsersMethod::class,
113        'user' => Method\UserMethod::class,
114        'user_preferences' => Method\UserPreferencesMethod::class,
115        'user_preference' => Method\UserPreferenceMethod::class,
116        'user_create' => Method\UserCreateMethod::class,
117        'user_update' => Method\UserUpdateMethod::class,
118        'user_delete' => Method\UserDeleteMethod::class,
119        'followers' => Method\FollowersMethod::class,
120        'following' => Method\FollowingMethod::class,
121        'toggle_follow' => Method\ToggleFollowMethod::class,
122        'last_shouts' => Method\LastShoutsMethod::class,
123        'rate' => Method\RateMethod::class,
124        'flag' => Method\FlagMethod::class,
125        'record_play' => Method\RecordPlayMethod::class,
126        'scrobble' => Method\ScrobbleMethod::class,
127        'catalogs' => Method\CatalogsMethod::class,
128        'catalog' => Method\CatalogMethod::class,
129        'catalog_action' => Method\CatalogActionMethod::class,
130        'catalog_file' => Method\CatalogFileMethod::class,
131        'timeline' => Method\TimelineMethod::class,
132        'friends_timeline' => Method\FriendsTimelineMethod::class,
133        'update_from_tags' => Method\UpdateFromTagsMethod::class,
134        'update_artist_info' => Method\UpdateArtistInfoMethod::class,
135        'update_art' => Method\UpdateArtMethod::class,
136        'update_podcast' => Method\UpdatePodcastMethod::class,
137        'stream' => Method\StreamMethod::class,
138        'download' => Method\DownloadMethod::class,
139        'get_art' => Method\GetArtMethod::class,
140        'localplay' => Method\LocalplayMethod::class,
141        'localplay_songs' => Method\LocalplaySongsMethod::class,
142        'democratic' => Method\DemocraticMethod::class,
143        'system_update' => Method\SystemUpdateMethod::class,
144        'system_preferences' => Method\SystemPreferencesMethod::class,
145        'system_preference' => Method\SystemPreferenceMethod::class,
146        'preference_create' => Method\PreferenceCreateMethod::class,
147        'preference_edit' => Method\PreferenceEditMethod::class,
148        'preference_delete' => Method\PreferenceDeleteMethod::class,
149        'deleted_songs' => Method\DeletedSongsMethod::class,
150        'deleted_videos' => Method\DeletedVideosMethod::class,
151        'deleted_podcast_episodes' => Method\DeletedPodcastEpisodesMethod::class,
152    ];
153
154    /**
155     * @var string $auth_version
156     */
157    public static $auth_version = '350001';
158
159    /**
160     * @var string $version
161     */
162    public static $version = '5.0.0';
163
164    /**
165     * @var Browse $browse
166     */
167    public static $browse = null;
168
169    public static function getBrowse(): Browse
170    {
171        if (self::$browse === null) {
172            self::$browse = new Browse(null, false);
173        }
174
175        return self::$browse;
176    }
177
178    /**
179     * message
180     * call the correct success message depending on format
181     * @param string $message
182     * @param string $format
183     * @param array $return_data
184     */
185    public static function message($message, $format = 'xml', $return_data = array())
186    {
187        switch ($format) {
188            case 'json':
189                echo Json_Data::success($message, $return_data);
190                break;
191            default:
192                echo Xml_Data::success($message, $return_data);
193        }
194    } // message
195
196    /**
197     * error
198     * call the correct error message depending on format
199     * @param string $message
200     * @param string $error_code
201     * @param string $method
202     * @param string $error_type
203     * @param string $format
204     */
205    public static function error($message, $error_code, $method, $error_type, $format = 'xml')
206    {
207        switch ($format) {
208            case 'json':
209                echo Json_Data::error($error_code, $message, $method, $error_type);
210                break;
211            default:
212                echo Xml_Data::error($error_code, $message, $method, $error_type);
213        }
214    } // error
215
216    /**
217     * empty
218     * call the correct empty message depending on format
219     * @param string $empty_type
220     * @param string $format
221     */
222    public static function empty($empty_type, $format = 'xml')
223    {
224        switch ($format) {
225            case 'json':
226                echo Json_Data::empty($empty_type);
227                break;
228            default:
229                echo Xml_Data::empty();
230        }
231    } // empty
232
233    /**
234     * set_filter
235     * MINIMUM_API_VERSION=380001
236     *
237     * This is a play on the browse function, it's different as we expose
238     * the filters in a slightly different and vastly simpler way to the
239     * end users--so we have to do a little extra work to make them work
240     * internally.
241     * @param string $filter
242     * @param integer|string|boolean|null $value
243     * @return boolean
244     */
245    public static function set_filter($filter, $value, ?Browse $browse = null)
246    {
247        if (!strlen((string)$value)) {
248            return false;
249        }
250
251        if ($browse === null) {
252            $browse = self::getBrowse();
253        }
254
255        switch ($filter) {
256            case 'add':
257                // Check for a range, if no range default to gt
258                if (strpos((string)$value, '/')) {
259                    $elements = explode('/', (string)$value);
260                    $browse->set_filter('add_lt', strtotime((string)$elements['1']));
261                    $browse->set_filter('add_gt', strtotime((string)$elements['0']));
262                } else {
263                    $browse->set_filter('add_gt', strtotime((string)$value));
264                }
265                break;
266            case 'update':
267                // Check for a range, if no range default to gt
268                if (strpos((string)$value, '/')) {
269                    $elements = explode('/', (string)$value);
270                    $browse->set_filter('update_lt', strtotime((string)$elements['1']));
271                    $browse->set_filter('update_gt', strtotime((string)$elements['0']));
272                } else {
273                    $browse->set_filter('update_gt', strtotime((string)$value));
274                }
275                break;
276            case 'alpha_match':
277                $browse->set_filter('alpha_match', $value);
278                break;
279            case 'exact_match':
280                $browse->set_filter('exact_match', $value);
281                break;
282            case 'enabled':
283                $browse->set_filter('enabled', $value);
284                break;
285            default:
286                break;
287        } // end filter
288
289        return true;
290    } // set_filter
291
292    /**
293     * check_parameter
294     *
295     * This function checks the $input actually has the parameter.
296     * Parameters must be an array of required elements as a string
297     *
298     * @param array $input
299     * @param string[] $parameters e.g. array('auth', type')
300     * @param string $method
301     * @return boolean
302     */
303    public static function check_parameter($input, $parameters, $method)
304    {
305        foreach ($parameters as $parameter) {
306            if ($input[$parameter] === 0 || $input[$parameter] === '0') {
307                continue;
308            }
309            if (empty($input[$parameter])) {
310                debug_event(__CLASS__, "'" . $parameter . "' required on " . $method . " function call.", 2);
311
312                /* HINT: Requested object string/id/type ("album", "myusername", "some song title", 1298376) */
313                self::error(sprintf(T_('Bad Request: %s'), $parameter), '4710', $method, 'system', $input['api_format']);
314
315                return false;
316            }
317        }
318
319        return true;
320    } // check_parameter
321
322    /**
323     * check_access
324     *
325     * This function checks the user can perform the function requested
326     * 'interface', 100, User::get_from_username(Session::username($input['auth']))->id)
327     *
328     * @param string $type
329     * @param integer $level
330     * @param integer $user_id
331     * @param string $method
332     * @param string $format
333     * @return boolean
334     */
335    public static function check_access($type, $level, $user_id, $method, $format = 'xml')
336    {
337        if (!Access::check($type, $level, $user_id)) {
338            debug_event(self::class, $type . " '" . $level . "' required on " . $method . " function call.", 2);
339            /* HINT: Access level, eg 75, 100 */
340            self::error(sprintf(T_('Require: %s'), $level), '4742', $method, 'account', $format);
341
342            return false;
343        }
344
345        return true;
346    } // check_access
347
348    /**
349     * server_details
350     *
351     * get the server counts for pings and handshakes
352     *
353     * @param string $token
354     * @return array
355     */
356    public static function server_details($token = '')
357    {
358        // We need to also get the 'last update' of the catalog information in an RFC 2822 Format
359        $sql        = 'SELECT MAX(`last_update`) AS `update`, MAX(`last_add`) AS `add`, MAX(`last_clean`) AS `clean` FROM `catalog`';
360        $db_results = Dba::read($sql);
361        $details    = Dba::fetch_assoc($db_results);
362
363        // Now we need to quickly get the totals
364        $client      = static::getUserRepository()->findByApiKey(trim($token));
365        $counts      = Catalog::get_server_counts($client->id);
366        $album_count = (Preference::get('album_group', $client->id))
367            ? (int)$counts['album_group']
368            : (int)$counts['album'];
369        $playlists = (AmpConfig::get('hide_search', false))
370            ? ((int)$counts['playlist'])
371            : ((int)$counts['playlist'] + (int)$counts['search']);
372        $autharray = (!empty($token)) ? array('auth' => $token) : array();
373
374        // send the totals
375        $outarray = array('api' => Api::$version,
376            'session_expire' => date("c", time() + AmpConfig::get('session_length', 3600) - 60),
377            'update' => date("c", (int)$details['update']),
378            'add' => date("c", (int)$details['add']),
379            'clean' => date("c", (int)$details['clean']),
380            'songs' => (int)$counts['song'],
381            'albums' => $album_count,
382            'artists' => (int)$counts['artist'],
383            'genres' => (int)$counts['tag'],
384            'playlists' => (int)$counts['playlist'],
385            'searches' => (int)$counts['search'],
386            'playlists_searches' => $playlists,
387            'users' => ((int)$counts['user'] + (int)$counts['user']),
388            'catalogs' => (int)$counts['catalog'],
389            'videos' => (int)$counts['video'],
390            'podcasts' => (int)$counts['podcast'],
391            'podcast_episodes' => (int)$counts['podcast_episode'],
392            'shares' => (int)$counts['share'],
393            'licenses' => (int)$counts['license'],
394            'live_streams' => (int)$counts['live_stream'],
395            'labels' => (int)$counts['label']);
396
397        return array_merge($autharray, $outarray);
398    } // server_details
399
400    /**
401     * @deprecated inject by constructor
402     */
403    private static function getUserRepository(): UserRepositoryInterface
404    {
405        global $dic;
406
407        return $dic->get(UserRepositoryInterface::class);
408    }
409}
410