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