"Fossies" - the Fresh Open Source Software Archive

Member "ampache-5.1.0/src/Module/Application/Playback/PlayAction.php" (25 Oct 2021, 41794 Bytes) of package /linux/www/ampache-5.1.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "PlayAction.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 5.0.0_vs_5.1.0.

    1 <?php
    2 
    3 /*
    4  * vim:set softtabstop=4 shiftwidth=4 expandtab:
    5  *
    6  * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
    7  * Copyright 2001 - 2020 Ampache.org
    8  *
    9  * This program is free software: you can redistribute it and/or modify
   10  * it under the terms of the GNU Affero General Public License as published by
   11  * the Free Software Foundation, either version 3 of the License, or
   12  * (at your option) any later version.
   13  *
   14  * This program is distributed in the hope that it will be useful,
   15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
   16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   17  * GNU Affero General Public License for more details.
   18  *
   19  * You should have received a copy of the GNU Affero General Public License
   20  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
   21  *
   22  */
   23 
   24 declare(strict_types=0);
   25 
   26 namespace Ampache\Module\Application\Playback;
   27 
   28 use Ampache\Config\AmpConfig;
   29 use Ampache\Module\Application\ApplicationActionInterface;
   30 use Ampache\Module\Application\Exception\AccessDeniedException;
   31 use Ampache\Module\Authentication\AuthenticationManagerInterface;
   32 use Ampache\Module\Authorization\AccessLevelEnum;
   33 use Ampache\Module\Authorization\Check\NetworkCheckerInterface;
   34 use Ampache\Module\Authorization\GuiGatekeeperInterface;
   35 use Ampache\Module\Playback\Stream;
   36 use Ampache\Module\Playback\Stream_Playlist;
   37 use Ampache\Module\Statistics\Stats;
   38 use Ampache\Module\System\Core;
   39 use Ampache\Module\System\Dba;
   40 use Ampache\Module\System\Session;
   41 use Ampache\Module\Util\Horde_Browser;
   42 use Ampache\Module\Util\ObjectTypeToClassNameMapper;
   43 use Ampache\Repository\Model\Broadcast;
   44 use Ampache\Repository\Model\Catalog;
   45 use Ampache\Repository\Model\Channel;
   46 use Ampache\Repository\Model\Democratic;
   47 use Ampache\Repository\Model\Live_Stream;
   48 use Ampache\Repository\Model\Podcast_Episode;
   49 use Ampache\Repository\Model\Preference;
   50 use Ampache\Repository\Model\Random;
   51 use Ampache\Repository\Model\Share;
   52 use Ampache\Repository\Model\Song;
   53 use Ampache\Repository\Model\Song_Preview;
   54 use Ampache\Repository\Model\User;
   55 use Ampache\Repository\Model\Video;
   56 use Ampache\Repository\SongRepositoryInterface;
   57 use Ampache\Repository\UserRepositoryInterface;
   58 use Psr\Http\Message\ResponseInterface;
   59 use Psr\Http\Message\ServerRequestInterface;
   60 
   61 final class PlayAction implements ApplicationActionInterface
   62 {
   63     public const REQUEST_KEY = 'play';
   64 
   65     private Horde_Browser $browser;
   66 
   67     private AuthenticationManagerInterface $authenticationManager;
   68 
   69     private NetworkCheckerInterface $networkChecker;
   70 
   71     private SongRepositoryInterface $songRepository;
   72 
   73     private UserRepositoryInterface $userRepository;
   74 
   75     public function __construct(
   76         Horde_Browser $browser,
   77         AuthenticationManagerInterface $authenticationManager,
   78         NetworkCheckerInterface $networkChecker,
   79         SongRepositoryInterface $songRepository,
   80         UserRepositoryInterface $userRepository
   81     ) {
   82         $this->browser               = $browser;
   83         $this->authenticationManager = $authenticationManager;
   84         $this->networkChecker        = $networkChecker;
   85         $this->songRepository        = $songRepository;
   86         $this->userRepository        = $userRepository;
   87     }
   88 
   89     public function run(ServerRequestInterface $request, GuiGatekeeperInterface $gatekeeper): ?ResponseInterface
   90     {
   91         ob_end_clean();
   92 
   93         //debug_event('play/index', print_r(apache_request_headers(), true), 5);
   94 
   95         /**
   96          * The following code takes a "beautiful" url, splits it into key/value pairs and
   97          * then replaces the PHP $_REQUEST as if the URL had arrived in un-beautified form.
   98          * (This is necessary to avoid some DLNA players barfing on the URL, particularly Windows Media Player)
   99          *
  100          * The reason for not trying to do the whole job in mod_rewrite is that there are typically
  101          * more than 10 arguments to this function now, and that's tricky with mod_rewrite's 10 arg limit
  102          */
  103         $slashcount = substr_count($_SERVER['QUERY_STRING'], '/');
  104         if ($slashcount > 2) {
  105             // e.g. ssid/3ca112fff23376ef7c74f018497dd39d/type/song/oid/280/uid/player/api/name/Glad.mp3
  106             $new_arr     = explode('/', $_SERVER['QUERY_STRING']);
  107             $new_request = array();
  108             $key         = null;
  109             $i           = 0;
  110             // alternate key and value through the split array e.g:
  111             // array('ssid', '3ca112fff23376ef7c74f018497dd39d', 'type', 'song', 'oid', '280', 'uid', 'player', 'api', 'name', 'Glad.mp3))
  112             foreach ($new_arr as $v) {
  113                 if ($i == 0) {
  114                     // key name
  115                     $key = $v;
  116                     $i   = 1;
  117                 } else {
  118                     // key value
  119                     $value = $v;
  120                     $i     = 0;
  121                     // set it now that you've set both
  122                     $new_request[$key] = $value;
  123                 }
  124             }
  125             $_REQUEST = $new_request;
  126         }
  127 
  128         /* These parameters had better come in on the url. */
  129         $action       = (string)filter_input(INPUT_GET, 'action', FILTER_SANITIZE_SPECIAL_CHARS);
  130         $stream_name  = (string)filter_input(INPUT_GET, 'name', FILTER_SANITIZE_SPECIAL_CHARS);
  131         $object_id    = (int)scrub_in(filter_input(INPUT_GET, 'oid', FILTER_SANITIZE_SPECIAL_CHARS));
  132         $uid          = (int)scrub_in(filter_input(INPUT_GET, 'uid', FILTER_SANITIZE_SPECIAL_CHARS));
  133         $session_id   = (string)scrub_in(filter_input(INPUT_GET, 'ssid', FILTER_SANITIZE_SPECIAL_CHARS));
  134         $type         = (string)scrub_in(filter_input(INPUT_GET, 'type', FILTER_SANITIZE_SPECIAL_CHARS));
  135         $client       = (string)scrub_in(filter_input(INPUT_GET, 'client', FILTER_SANITIZE_SPECIAL_CHARS));
  136         $cache        = (string)scrub_in(filter_input(INPUT_GET, 'cache', FILTER_SANITIZE_SPECIAL_CHARS));
  137         $format       = (string)scrub_in(filter_input(INPUT_GET, 'format', FILTER_SANITIZE_SPECIAL_CHARS));
  138         $bitrate      = (int)scrub_in(filter_input(INPUT_GET, 'bitrate', FILTER_SANITIZE_SPECIAL_CHARS));
  139         $original     = $format == 'raw';
  140         $transcode_to = (!$original && $format != '') ? $format : null;
  141         $player       = (string)scrub_in(filter_input(INPUT_GET, 'player', FILTER_SANITIZE_SPECIAL_CHARS));
  142         $record_stats = true;
  143         $use_auth     = AmpConfig::get('use_auth');
  144 
  145         // Share id and secret if used
  146         $share_id = (int)filter_input(INPUT_GET, 'share_id', FILTER_SANITIZE_NUMBER_INT);
  147         $secret   = (string)scrub_in(filter_input(INPUT_GET, 'share_secret', FILTER_SANITIZE_SPECIAL_CHARS));
  148 
  149         // This is specifically for tmp playlist requests
  150         $demo_id    = (string)scrub_in(filter_input(INPUT_GET, 'demo_id', FILTER_SANITIZE_SPECIAL_CHARS));
  151         $random     = (string)scrub_in(filter_input(INPUT_GET, 'random', FILTER_SANITIZE_SPECIAL_CHARS));
  152 
  153         // democratic play url doesn't include these
  154         if ($demo_id !== '') {
  155             $type   = 'song';
  156         }
  157         // if you don't specify, assume stream
  158         if (empty($action)) {
  159             $action = 'stream';
  160         }
  161         // allow disabling stat recording from the play url
  162         if (($action === 'download' || $cache === '1') || !in_array($type, array('song', 'video', 'podcast_episode'))) {
  163             debug_event('play/index', 'record_stats disabled: cache {' . $type . "}", 5);
  164             $action       = 'download';
  165             $record_stats = false;
  166         }
  167 
  168         $maxbitrate    = 0;
  169         $media_bitrate = 0;
  170         $resolution    = '';
  171         $quality       = 0;
  172         $time          = time();
  173 
  174         if (AmpConfig::get('transcode_player_customize') && !$original) {
  175             $transcode_to = $transcode_to ?? (string)scrub_in(filter_input(INPUT_GET, 'transcode_to', FILTER_SANITIZE_SPECIAL_CHARS));
  176 
  177             // Trick to avoid LimitInternalRecursion reconfiguration
  178             $vsettings = (string)scrub_in(filter_input(INPUT_GET, 'transcode_to', FILTER_SANITIZE_SPECIAL_CHARS));
  179             if (!empty($vsettings)) {
  180                 $vparts  = explode('-', $vsettings);
  181                 $v_count = count($vparts);
  182                 for ($i = 0; $i < $v_count; $i += 2) {
  183                     switch ($vparts[$i]) {
  184                         case 'maxbitrate':
  185                             $maxbitrate = (int) ($vparts[$i + 1]);
  186                             break;
  187                         case 'resolution':
  188                             $resolution = $vparts[$i + 1];
  189                             break;
  190                         case 'quality':
  191                             $quality = (int) ($vparts[$i + 1]);
  192                             break;
  193                     }
  194                 }
  195             }
  196         }
  197         $subtitle         = '';
  198         $send_full_stream = (string)AmpConfig::get('send_full_stream');
  199         $send_all_in_once = ($send_full_stream == 'true' || $send_full_stream === $player);
  200 
  201         if (!$type) {
  202             $type = 'song';
  203         }
  204 
  205         debug_event('play/index', "Asked for type {{$type}}", 5);
  206 
  207         if ($type == 'playlist') {
  208             $playlist_type = scrub_in($_REQUEST['playlist_type']);
  209             $object_id     = $session_id;
  210         }
  211 
  212         // First things first, if we don't have a uid/oid stop here
  213         // Added $session_id here as user may not be specified but then ssid may be and will be checked later
  214         if (empty($uid) && empty($session_id) && (!$share_id && !$secret)) {
  215             debug_event('play/index', 'No object UID specified, nothing to play', 2);
  216             header('HTTP/1.1 400 Nothing To Play');
  217 
  218             return null;
  219         }
  220 
  221         // Authenticate the user if specified
  222         $username = Core::get_server('PHP_AUTH_USER');
  223         if (empty($username)) {
  224             $username = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_SPECIAL_CHARS);
  225         }
  226         $password = Core::get_server('PHP_AUTH_PW');
  227         if (empty($password)) {
  228             $password = filter_input(INPUT_GET, 'p', FILTER_SANITIZE_SPECIAL_CHARS);
  229         }
  230         $apikey = filter_input(INPUT_GET, 'apikey', FILTER_SANITIZE_SPECIAL_CHARS);
  231 
  232         $user = null;
  233         // If explicit user authentication was passed
  234         $user_authenticated = false;
  235         if (!empty($apikey)) {
  236             $user = $this->userRepository->findByApiKey(trim($apikey));
  237             if ($user != null) {
  238                 $GLOBALS['user'] = $user;
  239                 $uid             = $user->id;
  240                 Preference::init();
  241                 $user_authenticated = true;
  242             }
  243         } elseif (!empty($username) && !empty($password)) {
  244             $auth = $this->authenticationManager->login($username, $password);
  245             if ($auth['success']) {
  246                 $user            = User::get_from_username($auth['username']);
  247                 $GLOBALS['user'] = $user;
  248                 $uid             = $user->id;
  249                 Preference::init();
  250                 $user_authenticated = true;
  251             }
  252         }
  253 
  254         if (empty($uid) && (!$share_id && !$secret)) {
  255             debug_event('play/index', 'No user specified', 2);
  256             header('HTTP/1.1 400 No User Specified');
  257 
  258             return null;
  259         }
  260 
  261         $session_name = AmpConfig::get('session_name');
  262         if ($use_auth) {
  263             // Identify the user according to it's web session
  264             // We try to avoid the generic 'Ampache User' as much as possible
  265             if (array_key_exists($session_name, $_COOKIE) && Session::exists('interface', $_COOKIE[$session_name])) {
  266                 Session::check();
  267                 $user = User::get_from_username($_SESSION['userdata']['username']);
  268                 $uid  = $user->id;
  269             }
  270         }
  271 
  272         if (!$share_id) {
  273             // No explicit authentication, use session
  274             if (!$user_authenticated) {
  275                 $user = $GLOBALS['user'] = new User($uid);
  276                 Preference::init();
  277 
  278                 /* If the user has been disabled (true value) */
  279                 if (make_bool($user->disabled)) {
  280                     debug_event('play/index', $user->username . " is currently disabled, stream access denied", 3);
  281                     header('HTTP/1.1 403 User disabled');
  282 
  283                     return null;
  284                 }
  285 
  286                 // If require_session is set then we need to make sure we're legit
  287                 if ($use_auth && AmpConfig::get('require_session')) {
  288                     if (!AmpConfig::get('require_localnet_session') && $this->networkChecker->check(AccessLevelEnum::TYPE_NETWORK, Core::get_global('user')->id, AccessLevelEnum::LEVEL_GUEST)) {
  289                         debug_event('play/index', 'Streaming access allowed for local network IP ' . Core::get_server('REMOTE_ADDR'), 4);
  290                     } elseif (!Session::exists('stream', $session_id)) {
  291                         // No valid session id given, try with cookie session from web interface
  292                         $session_id = $_COOKIE[$session_name];
  293                         if (!Session::exists('interface', $session_id)) {
  294                             debug_event('play/index', "Streaming access denied: Session $session_id has expired", 3);
  295                             header('HTTP/1.1 403 Session Expired');
  296 
  297                             return null;
  298                         }
  299                     }
  300                     // Now that we've confirmed the session is valid extend it
  301                     Session::extend($session_id, 'stream');
  302                 }
  303             }
  304 
  305             /* Update the users last seen information */
  306             $this->userRepository->updateLastSeen(
  307                 (int) $user->id,
  308                 time()
  309             );
  310         } else {
  311             $uid   = 0;
  312             $share = new Share((int) $share_id);
  313 
  314             if (!$share->is_valid($secret, 'stream')) {
  315                 header('HTTP/1.1 403 Access Unauthorized');
  316 
  317                 return null;
  318             }
  319 
  320             if (!$this->is_shared_media($share, $object_id)) {
  321                 header('HTTP/1.1 403 Access Unauthorized');
  322 
  323                 return null;
  324             }
  325 
  326             $user = $GLOBALS['user'] = new User($share->user);
  327             Preference::init();
  328         }
  329 
  330         // If we are in demo mode.. die here
  331         if (AmpConfig::get('demo_mode')) {
  332             throw new AccessDeniedException(
  333                 'Streaming Access Denied: Disable demo_mode in \'config/ampache.cfg.php\''
  334             );
  335         }
  336         // Check whether streaming is allowed
  337         $prefs = AmpConfig::get('allow_stream_playback') && $_SESSION['userdata']['preferences']['allow_stream_playback'];
  338         if (!$prefs) {
  339             throw new AccessDeniedException(
  340                 'Streaming Access Denied: Enable \'Allow Streaming\' in Server Config -> Options'
  341             );
  342         }
  343 
  344         // If they are using access lists let's make sure that they have enough access to play this mojo
  345         if (AmpConfig::get('access_control')) {
  346             if (
  347                 !$this->networkChecker->check(AccessLevelEnum::TYPE_STREAM, Core::get_global('user')->id) &&
  348                 !$this->networkChecker->check(AccessLevelEnum::TYPE_NETWORK, Core::get_global('user')->id)
  349             ) {
  350                 throw new AccessDeniedException(
  351                     sprintf('Streaming Access Denied: %s does not have stream level access', Core::get_user_ip())
  352                 );
  353             }
  354         } // access_control is enabled
  355 
  356         // Handle playlist downloads
  357         if ($type == 'playlist' && isset($playlist_type)) {
  358             $playlist = new Stream_Playlist($object_id);
  359             // Some rudimentary security
  360             if ($uid != $playlist->user) {
  361                 throw new AccessDeniedException();
  362             }
  363 
  364             return $playlist->generate_playlist($playlist_type);
  365         }
  366 
  367         /**
  368          * If we've got a tmp playlist then get the
  369          * current song, and do any other crazyness
  370          * we need to
  371          */
  372         if ($demo_id !== '') {
  373             $democratic = new Democratic($demo_id);
  374             $democratic->set_parent();
  375 
  376             // If there is a cooldown we need to make sure this song isn't a repeat
  377             if (!$democratic->cooldown) {
  378                 /* This takes into account votes etc and removes the */
  379                 $object_id = $democratic->get_next_object();
  380             } else {
  381                 // Pull history
  382                 $song_cool_check = 0;
  383                 $object_id       = $democratic->get_next_object($song_cool_check);
  384                 $object_ids      = $democratic->get_cool_songs();
  385                 while (in_array($object_id, $object_ids)) {
  386                     $song_cool_check++;
  387                     $object_id = $democratic->get_next_object($song_cool_check);
  388                     if ($song_cool_check >= '5') {
  389                         break;
  390                     }
  391                 } // while we've got the 'new' song in old the array
  392             } // end if we've got a cooldown
  393         } // if democratic ID passed
  394 
  395         /**
  396          * if we are doing random let's pull the random object
  397          */
  398         if ($random !== '') {
  399             if ((int) Core::get_request('start') < 1) {
  400                 if (array_key_exists('random_type', $_REQUEST)) {
  401                     $rtype = $_REQUEST['random_type'];
  402                 } else {
  403                     $rtype = $type;
  404                 }
  405                 $object_id = Random::get_single_song($rtype);
  406                 if ($object_id) {
  407                     // Save this one in case we do a seek
  408                     $_SESSION['random']['last'] = $object_id;
  409                 }
  410             } else {
  411                 $object_id = $_SESSION['random']['last'];
  412             }
  413         } // if random
  414 
  415         if ($type == 'song') {
  416             /* Base Checks passed create the song object */
  417             $media = new Song($object_id);
  418         } elseif ($type == 'song_preview') {
  419             $media = new Song_Preview($object_id);
  420         } elseif ($type == 'podcast_episode') {
  421             $media = new Podcast_Episode((int) $object_id);
  422         } else {
  423             $type  = 'video';
  424             $media = new Video($object_id);
  425             if (array_key_exists('subtitle', $_REQUEST)) {
  426                 $subtitle = $media->get_subtitle_file($_REQUEST['subtitle']);
  427             }
  428         }
  429         $media->format();
  430 
  431         if (!User::stream_control(array(array('object_type' => $type, 'object_id' => $media->id)))) {
  432             throw new AccessDeniedException(
  433                 sprintf(
  434                     'Stream control failed for user %s on %s',
  435                     Core::get_global('user')->username,
  436                     $media->get_stream_name()
  437                 )
  438             );
  439         }
  440 
  441         $cache_path     = (string)AmpConfig::get('cache_path', '');
  442         $cache_target   = AmpConfig::get('cache_target', '');
  443         $cache_file     = false;
  444         $file_target    = false;
  445         $mediaCatalogId = $media->catalog ?? null;
  446         if ($mediaCatalogId) {
  447             /* If the media is disabled */
  448             if (isset($media->enabled) && !make_bool($media->enabled)) {
  449                 debug_event('play/index', "Error: " . $media->file . " is currently disabled, song skipped", 3);
  450                 // Check to see if this is a democratic playlist, if so remove it completely
  451                 if ($demo_id !== '' && isset($democratic)) {
  452                     $democratic->delete_from_oid($object_id, $type);
  453                 }
  454                 header('HTTP/1.1 404 File disabled');
  455 
  456                 return null;
  457             }
  458             // The media catalog is restricted
  459             if (!Catalog::has_access($mediaCatalogId, $user->id)) {
  460                 debug_event('play/index', "Error: You are not allowed to play $media->file", 3);
  461 
  462                 return null;
  463             }
  464             // If we are running in Legalize mode, don't play medias already playing
  465             if (AmpConfig::get('lock_songs')) {
  466                 if (!Stream::check_lock_media($media->id, $type)) {
  467                     return null;
  468                 }
  469             }
  470             $file_target = rtrim(trim($cache_path), '/') . '/' . $mediaCatalogId . '/' . $media->id . '.' . $cache_target;
  471             if (!empty($cache_path) && !empty($cache_target) && is_file($file_target)) {
  472                 debug_event('play/index', 'Found pre-cached file {' . $file_target . '}', 5);
  473                 $cache_file   = true;
  474                 $original     = true;
  475                 $media->file  = $file_target;
  476                 $media->size  = Core::get_filesize($file_target);
  477                 $media->type  = $cache_target;
  478                 $transcode_to = false;
  479             } else {
  480                 // Build up the catalog for our current object
  481                 $catalog = Catalog::create_from_id($mediaCatalogId);
  482                 $media   = $catalog->prepare_media($media);
  483             }
  484         } else {
  485             // No catalog, must be song preview or something like that => just redirect to file
  486             if ($type == "song_preview") {
  487                 $media->stream();
  488             } else {
  489                 header('Location: ' . $media->file);
  490 
  491                 return null;
  492             }
  493         }
  494         if ($media == null) {
  495             // Handle democratic removal
  496             if ($demo_id !== '' && isset($democratic)) {
  497                 $democratic->delete_from_oid($object_id, $type);
  498             }
  499 
  500             return null;
  501         }
  502         // load the cache file or the local file
  503         $stream_file = ($cache_file && $file_target) ? $file_target : $media->file;
  504 
  505         /* If we don't have a file, or the file is not readable */
  506         if (!$stream_file || !Core::is_readable(Core::conv_lc_file($stream_file))) {
  507             // We need to make sure this isn't democratic play, if it is then remove the media from the vote list
  508             if (!empty($tmp_playlist)) {
  509                 $tmp_playlist->delete_track($object_id);
  510             }
  511             // FIXME: why are these separate?
  512             // Remove the media votes if this is a democratic song
  513             if ($demo_id !== '' && isset($democratic)) {
  514                 $democratic->delete_from_oid($object_id, $type);
  515             }
  516 
  517             debug_event('play/index', "Media " . $stream_file . " ($media->title) does not have a valid filename specified", 2);
  518             header('HTTP/1.1 404 Invalid media, file not found or file unreadable');
  519 
  520             return null;
  521         }
  522 
  523         // don't abort the script if user skips this media because we need to update now_playing
  524         ignore_user_abort(true);
  525 
  526         // Format the media name
  527         $media_name = $stream_name ?? $media->get_stream_name() . "." . $media->type;
  528 
  529         header('Access-Control-Allow-Origin: *');
  530 
  531         $sessionkey = $session_id ?: Stream::get_session();
  532         $agent      = (!empty($client))
  533             ? $client
  534             : Session::agent($sessionkey);
  535         $location   = Session::get_geolocation($sessionkey);
  536 
  537         /* If they are just trying to download make sure they have rights
  538          * and then present them with the download file
  539          */
  540         if ($action == 'download' && !$original) {
  541             debug_event('play/index', 'Downloading transcoded file... ' . $transcode_to, 4);
  542             if (!$share_id) {
  543                 if (Core::get_server('REQUEST_METHOD') != 'HEAD' && $record_stats) {
  544                     debug_event('play/index', 'Registering download stats for {' . $media->get_stream_name() . '}...', 5);
  545                     Stats::insert($type, $media->id, $uid, $agent, $location, 'download', $time);
  546                 }
  547             }
  548             $record_stats = false;
  549         } elseif ($action == 'download' && AmpConfig::get('download')) {
  550             debug_event('play/index', 'Downloading raw file...', 4);
  551             // STUPID IE
  552             $media_name = str_replace(array('?', '/', '\\'), "_", $media->f_file);
  553 
  554             $headers = $this->browser->getDownloadHeaders($media_name, $media->mime, false, $media->size);
  555 
  556             foreach ($headers as $headerName => $value) {
  557                 header(sprintf('%s: %s', $headerName, $value));
  558             }
  559 
  560             $filepointer   = fopen(Core::conv_lc_file($stream_file), 'rb');
  561             $bytesStreamed = 0;
  562 
  563             if (!is_resource($filepointer)) {
  564                 debug_event('play/index', "Error: Unable to open " . $stream_file . " for downloading", 2);
  565 
  566                 return null;
  567             }
  568 
  569             if (!$share_id) {
  570                 if (Core::get_server('REQUEST_METHOD') != 'HEAD' && $record_stats) {
  571                     debug_event('play/index', 'Registering download stats for {' . $media->get_stream_name() . '}...', 5);
  572                     Stats::insert($type, $media->id, $uid, $agent, $location, 'download', $time);
  573                 }
  574             } else {
  575                 Stats::insert($type, $media->id, $uid, 'share.php', array(), 'download', $time);
  576             }
  577 
  578             // Check to see if we should be throttling because we can get away with it
  579             if (AmpConfig::get('rate_limit') > 0) {
  580                 while (!feof($filepointer)) {
  581                     echo fread($filepointer, (int) (round(AmpConfig::get('rate_limit') * 1024)));
  582                     $bytesStreamed += round(AmpConfig::get('rate_limit') * 1024);
  583                     flush();
  584                     sleep(1);
  585                 }
  586             } else {
  587                 fpassthru($filepointer);
  588             }
  589 
  590             fclose($filepointer);
  591 
  592             return null;
  593         } // if they are trying to download and they can
  594 
  595         // Prevent the script from timing out
  596         set_time_limit(0);
  597 
  598         // We're about to start. Record this user's IP.
  599         if (AmpConfig::get('track_user_ip')) {
  600             Core::get_global('user')->insert_ip_history();
  601         }
  602 
  603         $force_downsample = false;
  604         if (AmpConfig::get('downsample_remote')) {
  605             if (!$this->networkChecker->check(AccessLevelEnum::TYPE_NETWORK, Core::get_global('user')->id, AccessLevelEnum::LEVEL_DEFAULT)) {
  606                 debug_event('play/index', 'Downsampling enabled for non-local address ' . Core::get_server('REMOTE_ADDR'), 5);
  607                 $force_downsample = true;
  608             }
  609         }
  610 
  611         debug_event('play/index', $action . ' file (' . $stream_file . '}...', 5);
  612         debug_event('play/index', 'Media type {' . $media->type . '}', 5);
  613 
  614         $cpaction = filter_input(INPUT_GET, 'custom_play_action', FILTER_SANITIZE_SPECIAL_CHARS);
  615         if ($cpaction) {
  616             debug_event('play/index', 'Custom play action {' . $cpaction . '}', 5);
  617         }
  618         // Determine whether to transcode
  619         $transcode = false;
  620         // transcode_to should only have an effect if the media is the wrong format
  621         $transcode_to = $transcode_to == $media->type ? null : $transcode_to;
  622         if ($transcode_to) {
  623             debug_event('play/index', 'Transcode to {' . (string) $transcode_to . '}', 5);
  624         }
  625 
  626         // If custom play action or already cached, do not try to transcode
  627         if (!$cpaction && !$original && !$cache_file) {
  628             $transcode_cfg = AmpConfig::get('transcode');
  629             $valid_types   = $media->get_stream_types($player);
  630             if (!is_array($valid_types)) {
  631                 $valid_types = array($valid_types);
  632             }
  633             if ($transcode_cfg != 'never' && in_array('transcode', $valid_types) && $type !== 'podcast_episode') {
  634                 if ($transcode_to) {
  635                     $transcode = true;
  636                     debug_event('play/index', 'Transcoding due to explicit request for ' . (string) $transcode_to, 5);
  637                 } elseif ($transcode_cfg == 'always') {
  638                     $transcode = true;
  639                     debug_event('play/index', 'Transcoding due to always', 5);
  640                 } elseif ($force_downsample) {
  641                     $transcode = true;
  642                     debug_event('play/index', 'Transcoding due to downsample_remote', 5);
  643                 } else {
  644                     $media_bitrate = floor($media->bitrate / 1000);
  645                     // debug_event('play/index', "requested bitrate $bitrate <=> $media_bitrate ({$media->bitrate}) media bitrate", 5);
  646                     if (($bitrate > 0 && $bitrate < $media_bitrate) || ($maxbitrate > 0 && $maxbitrate < $media_bitrate)) {
  647                         $transcode = true;
  648                         debug_event('play/index', 'Transcoding because explicit bitrate request', 5);
  649                     } elseif (!in_array('native', $valid_types) && $action != 'download') {
  650                         $transcode = true;
  651                         debug_event('play/index', 'Transcoding because native streaming is unavailable', 5);
  652                     } elseif (!empty($subtitle)) {
  653                         $transcode = true;
  654                         debug_event('play/index', 'Transcoding because subtitle requested', 5);
  655                     }
  656                 }
  657             } else {
  658                 if ($transcode_cfg != 'never') {
  659                     debug_event('play/index', 'Transcoding is not enabled for this media type. Valid types: {' . json_encode($valid_types) . '}', 4);
  660                 } else {
  661                     debug_event('play/index', 'Transcode disabled in user settings.', 5);
  662                 }
  663             }
  664         }
  665 
  666         $troptions = array();
  667         if ($transcode) {
  668             if ($bitrate) {
  669                 $troptions['bitrate'] = ($maxbitrate > 0 && $maxbitrate < $media_bitrate) ? $maxbitrate : $bitrate;
  670             }
  671             if ($maxbitrate > 0) {
  672                 $troptions['maxbitrate'] = $maxbitrate;
  673             }
  674             if ($subtitle) {
  675                 $troptions['subtitle'] = $subtitle;
  676             }
  677             if ($resolution) {
  678                 $troptions['resolution'] = $resolution;
  679             }
  680             if ($quality) {
  681                 $troptions['quality'] = $quality;
  682             }
  683 
  684             if (array_key_exists('frame', $_REQUEST)) {
  685                 $troptions['frame'] = (float) $_REQUEST['frame'];
  686                 if (array_key_exists('duration', $_REQUEST)) {
  687                     $troptions['duration'] = (float) $_REQUEST['duration'];
  688                 }
  689             } elseif (array_key_exists('segment', $_REQUEST)) {
  690                 // 10 seconds segment. Should it be an option?
  691                 $ssize            = 10;
  692                 $send_all_in_once = true; // Should we use temporary folder instead?
  693                 debug_event('play/index', 'Sending all data in one piece.', 5);
  694                 $troptions['frame']    = (int) ($_REQUEST['segment']) * $ssize;
  695                 $troptions['duration'] = ($troptions['frame'] + $ssize <= $media->time) ? $ssize : ($media->time - $troptions['frame']);
  696             }
  697 
  698             $transcoder  = Stream::start_transcode($media, $transcode_to, $player, $troptions);
  699             $filepointer = $transcoder['handle'] ?? null;
  700             $media_name  = $media->f_artist_full . " - " . $media->title . "." . ($transcoder['format'] ?? '');
  701         } else {
  702             if ($cpaction) {
  703                 $transcoder  = $media->run_custom_play_action($cpaction, $transcode_to);
  704                 $filepointer = $transcoder['handle'] ?? null;
  705                 $transcode   = true;
  706             } else {
  707                 $filepointer = fopen(Core::conv_lc_file($stream_file), 'rb');
  708             }
  709         }
  710         //debug_event('play/index', 'troptions ' . print_r($troptions, true), 5);
  711 
  712         if ($transcode && ($media->bitrate > 0 && $media->time > 0)) {
  713             // Content-length guessing if required by the player.
  714             // Otherwise it shouldn't be used as we are not really sure about final length when transcoding
  715             $transcode_to = Song::get_transcode_settings_for_media(
  716                 (string) $media->type,
  717                 $transcode_to,
  718                 $player,
  719                 (string) $media->type,
  720                 $troptions
  721             )['format'];
  722             $maxbitrate   = Stream::get_max_bitrate($media, $transcode_to, $player, $troptions);
  723             if (Core::get_request('content_length') == 'required') {
  724                 if ($media->time > 0 && $maxbitrate > 0) {
  725                     $stream_size = ($media->time * $maxbitrate * 1000) / 8;
  726                 } else {
  727                     debug_event('play/index', 'Bad media duration / Max bitrate. Content-length calculation skipped.', 5);
  728                     $stream_size = null;
  729                 }
  730             } elseif ($transcode_to == 'mp3') {
  731                 // mp3 seems to be the only codec that calculates properly
  732                 $stream_rate = ($maxbitrate < floor($media->bitrate / 1000))
  733                     ? $maxbitrate
  734                     : floor($media->bitrate / 1000);
  735                 $stream_size = ($media->time * $stream_rate * 1000) / 8;
  736             } else {
  737                 $stream_size = null;
  738                 $maxbitrate  = 0;
  739             }
  740         } else {
  741             $stream_size = $media->size;
  742         }
  743 
  744         if (!is_resource($filepointer)) {
  745             debug_event('play/index', "Failed to open " . $stream_file . " for streaming", 2);
  746 
  747             return null;
  748         }
  749 
  750         if (!$transcode) {
  751             header('ETag: ' . $media->id);
  752         }
  753         // Handle Content-Range
  754 
  755         $start        = 0;
  756         $end          = 0;
  757         $range_values = sscanf(Core::get_server('HTTP_RANGE'), "bytes=%d-%d", $start, $end);
  758 
  759         if ($range_values > 0 && ($start > 0 || $end > 0)) {
  760             // Calculate stream size from byte range
  761             if ($range_values >= 2) {
  762                 $end = min($end, $media->size - 1);
  763             } else {
  764                 $end = $media->size - 1;
  765             }
  766             $stream_size = ($end - $start) + 1;
  767 
  768             if ($stream_size == null) {
  769                 debug_event('play/index', 'Content-Range header received, which we cannot fulfill due to unknown final length (transcoding?)', 2);
  770             } elseif (!$transcode) {
  771                 debug_event('play/index', 'Content-Range header received, skipping ' . $start . ' bytes out of ' . $media->size, 5);
  772                 fseek($filepointer, $start);
  773 
  774                 $range = $start . '-' . $end . '/' . $media->size;
  775                 header('HTTP/1.1 206 Partial Content');
  776                 header('Content-Range: bytes ' . $range);
  777             }
  778         }
  779 
  780         if (!isset($_REQUEST['segment'])) {
  781             if ($media->time) {
  782                 header('X-Content-Duration: ' . $media->time);
  783             }
  784 
  785             // Stats registering must be done before play. Do not move it.
  786             // It can be slow because of scrobbler plugins (lastfm, ...)
  787             if ($start > 0) {
  788                 debug_event('play/index', 'Content-Range doesn\'t start from 0, stats should already be registered previously; not collecting stats', 5);
  789             } else {
  790                 if (($action != 'download') && $record_stats) {
  791                     Stream::insert_now_playing((int) $media->id, (int) $uid, (int) $media->time, $session_id, ObjectTypeToClassNameMapper::reverseMap(get_class($media)));
  792                 }
  793                 if (!$share_id && $record_stats) {
  794                     if (Core::get_server('REQUEST_METHOD') != 'HEAD') {
  795                         debug_event('play/index', 'Registering stream @' . $time . ' for ' . $uid . ': ' . $media->get_stream_name() . ' {' . $media->id . '}', 4);
  796                         // internal scrobbling (user_activity and object_count tables)
  797                         if ($media->set_played($uid, $agent, $location, $time) && $user->id && get_class($media) === Song::class) {
  798                             // scrobble plugins
  799                             User::save_mediaplay($user, $media);
  800                         }
  801                     }
  802                 } elseif (!$share_id && $record_stats) {
  803                     if (Core::get_server('REQUEST_METHOD') != 'HEAD') {
  804                         debug_event('play/index', 'Registering download for ' . $uid . ': ' . $media->get_stream_name() . ' {' . $media->id . '}', 5);
  805                         Stats::insert($type, $media->id, $uid, $agent, $location, 'download', $time);
  806                     }
  807                 } elseif ($share_id) {
  808                     // shares are people too
  809                     $media->set_played(0, 'share.php', array(), $time);
  810                 }
  811             }
  812         }
  813 
  814         if ($transcode || $demo_id) {
  815             header('Accept-Ranges: none');
  816         } else {
  817             header('Accept-Ranges: bytes');
  818         }
  819 
  820         $mime = $media->mime;
  821         if ($transcode && isset($transcoder)) {
  822             $mime = ($type == 'video')
  823                 ? Video::type_to_mime($transcoder['format'])
  824                 : Song::type_to_mime($transcoder['format']);
  825             // Non-blocking stream doesn't work in Windows (php bug since 2005 and still here in 2020...)
  826             // We don't want to wait indefinitely for a potential error so we just ignore it.
  827             // https://bugs.php.net/bug.php?id=47918
  828             if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
  829                 // This to avoid hang, see http://php.net/manual/en/function.proc-open.php#89338
  830                 $transcode_error = fread($transcoder['stderr'], 4096);
  831                 if (!empty($transcode_error)) {
  832                     debug_event('play/index', 'Transcode stderr: ' . $transcode_error, 1);
  833                 }
  834                 fclose($transcoder['stderr']);
  835             }
  836         }
  837 
  838         // If this is a democratic playlist remove the entry.
  839         // We do this regardless of play amount.
  840         if ($demo_id && isset($democratic)) {
  841             $democratic->delete_from_oid($object_id, $type);
  842         }
  843 
  844         // Close sql connection
  845         // Warning: do not call functions requiring sql after this point
  846         Dba::disconnect();
  847         // Free the session write lock
  848         // Warning: Do not change any session variable after this call
  849         session_write_close();
  850 
  851         $headers = $this->browser->getDownloadHeaders($media_name, $mime, false, $stream_size);
  852 
  853         foreach ($headers as $headerName => $value) {
  854             header(sprintf('%s: %s', $headerName, $value));
  855         }
  856 
  857         $bytes_streamed = 0;
  858 
  859         // Actually do the streaming
  860         $buf_all = '';
  861         $r_arr   = array($filepointer);
  862         $w_arr   = $e_arr = array();
  863         $status  = stream_select($r_arr, $w_arr, $e_arr, 2);
  864         if ($status === false) {
  865             debug_event('play/index', 'stream_select failed.', 1);
  866         } elseif ($status > 0) {
  867             do {
  868                 $read_size = $transcode ? 2048 : min(2048, $stream_size - $bytes_streamed);
  869                 if ($buf = fread($filepointer, $read_size)) {
  870                     if ($send_all_in_once) {
  871                         $buf_all .= $buf;
  872                     } elseif (!empty($buf)) {
  873                         print($buf);
  874                         if (ob_get_length()) {
  875                             ob_flush();
  876                             flush();
  877                             ob_end_flush();
  878                         }
  879                         ob_start();
  880                     }
  881                     $bytes_streamed += strlen($buf);
  882                 }
  883             } while (!feof($filepointer) && (connection_status() == 0) && ($transcode || $bytes_streamed < $stream_size));
  884         }
  885 
  886         if ($send_all_in_once && connection_status() == 0) {
  887             header("Content-Length: " . strlen($buf_all));
  888             print($buf_all);
  889             ob_flush();
  890         }
  891 
  892         $real_bytes_streamed = $bytes_streamed;
  893         // Need to make sure enough bytes were sent.
  894         if ($bytes_streamed < $stream_size && (connection_status() == 0)) {
  895             print(str_repeat(' ', $stream_size - $bytes_streamed));
  896             $bytes_streamed = $stream_size;
  897         }
  898 
  899         fclose($filepointer);
  900         if ($transcode && isset($transcoder)) {
  901             Stream::kill_process($transcoder);
  902         }
  903 
  904         debug_event('play/index', 'Stream ended at ' . $bytes_streamed . ' (' . $real_bytes_streamed . ') bytes out of ' . $stream_size, 5);
  905 
  906         return null;
  907     }
  908 
  909     private function is_shared_media(Share $share, $media_id): bool
  910     {
  911         $is_shared = false;
  912         switch ($share->object_type) {
  913             case 'album':
  914                 $class_name = ObjectTypeToClassNameMapper::map($share->object_type);
  915                 $object     = new $class_name($share->object_id);
  916                 $songs      = $this->songRepository->getByAlbum((int) $object->id);
  917 
  918                 foreach ($songs as $songid) {
  919                     $is_shared = ($media_id == $songid);
  920                     if ($is_shared) {
  921                         break;
  922                     }
  923                 }
  924                 break;
  925             case 'playlist':
  926                 $class_name = ObjectTypeToClassNameMapper::map($share->object_type);
  927                 $object     = new $class_name($share->object_id);
  928                 $songs      = $object->get_songs();
  929                 foreach ($songs as $songid) {
  930                     $is_shared = ($media_id == $songid);
  931                     if ($is_shared) {
  932                         break;
  933                     }
  934                 }
  935                 break;
  936             default:
  937                 $is_shared = (($share->object_type == 'song' || $share->object_type == 'video') && $share->object_id == $media_id);
  938                 break;
  939         }
  940 
  941         return $is_shared;
  942     }
  943 }