<?php
/**
* @brief Downloads Files API
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage Downloads
* @since 8 Dec 2015
*/
namespace IPS\downloads\api;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* @brief Downloads Files API
*/
class _files extends \IPS\Content\Api\ItemController
{
/**
* Class
*/
protected $class = 'IPS\downloads\File';
/**
* GET /downloads/files
* Get list of files
*
* @note For requests using an OAuth Access Token for a particular member, only files the authorized user can view will be included
* @apiparam string categories Comma-delimited list of category IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only files started by those members are returned
* @apiparam int locked If 1, only files which are locked are returned, if 0 only unlocked
* @apiparam int hidden If 1, only files which are hidden are returned, if 0 only not hidden
* @apiparam int pinned If 1, only files which are pinned are returned, if 0 only not pinned
* @apiparam int featured If 1, only files which are featured are returned, if 0 only not featured
* @apiparam string sortBy What to sort by. Can be 'date' for creation date, 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\downloads\File>
*/
public function GETindex()
{
/* Where clause */
$where = array();
/* Return */
return $this->_list( $where, 'categories' );
}
/**
* GET /downloads/files/{id}
* View information about a specific file
*
* @param int $id ID Number
* @apiparam int version If specified, will show a previous version of a file (see GET /downloads/files/{id}/versions)
* @throws 2S303/1 INVALID_ID The file ID does not exist or the authorized user does not have permission to view it
* @throws 2S303/1 INVALID_VERSION The version ID does not exist
* @return \IPS\downloads\File
*/
public function GETitem( $id )
{
try
{
$file = \IPS\downloads\File::load( $id );
if ( $this->member and !$file->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
if ( isset( \IPS\Request::i()->version ) )
{
try
{
$backup = \IPS\Db::i()->select( '*', 'downloads_filebackup', array( 'b_id=? AND b_fileid=?', \IPS\Request::i()->version, $file->id ) )->first();
return new \IPS\Api\Response( 200, $file->apiOutput( $this->member, $backup ) );
}
catch ( \UnderflowException $e )
{
throw new \IPS\Api\Exception( 'INVALID_VERSION', '2S303/6', 404 );
}
}
else
{
return new \IPS\Api\Response( 200, $file->apiOutput( $this->member ) );
}
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/1', 404 );
}
}
/**
* POST /downloads/files
* Upload a file
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock files).
* @reqapiparam int category The ID number of the calendar the file should be created in
* @reqapiparam int author The ID number of the member creating the file (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @reqapiparam string title The file name
* @reqapiparam string description The description as HTML (e.g. "<p>This is an file.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam string version The version number
* @reqapiparam object files Files. Keys should be filename (e.g. 'file.txt') and values should be file content
* @apiparam object screenshots Screenshots. Keys should be filename (e.g. 'screenshot1.png') and values should be file content.
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the file post date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the file. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the file should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int pinned 1/0 indicating if the file should be featured
* @apiparam int featured 1/0 indicating if the file should be featured
* @throws 1S303/7 NO_CATEGEORY The category ID does not exist
* @throws 1S303/8 NO_AUTHOR The author ID does not exist
* @throws 1S303/9 NO_TITLE No title was supplied
* @throws 1S303/A NO_DESC No description was supplied
* @throws 1S303/B NO_FILES No files were supplied
* @throws 2S303/H NO_PERMISSION The authorized user does not have permission to create a file in that category
* @throws 1S303/I BAD_FILE_EXT One of the files has a file type that is not allowed
* @throws 1S303/J BAD_FILE_SIZE One of the files is too big
* @throws 1S303/K BAD_SS One of the screenshots is not a valid image
* @throws 1S303/L BAD_SS_SIZE One of the screenshots is too big by filesize
* @throws 1S303/M BAD_SS_DIMS One of the screenshots is too big by dimensions
* @throws 1S303/N NO_SS No screenshots are provided, but screenshots are required for the category
* @return \IPS\downloads\File
*/
public function POSTindex()
{
/* Get category */
try
{
$category = \IPS\downloads\Category::load( \IPS\Request::i()->category );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_CATEGEORY', '1S303/7', 400 );
}
/* Get author */
if ( $this->member )
{
if ( !$category->can( 'add', $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2S303/H', 403 );
}
$author = $this->member;
}
else
{
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S303/8', 400 );
}
}
else
{
$author = new \IPS\Member;
}
}
/* Check we have a title and a description */
if ( !\IPS\Request::i()->title )
{
throw new \IPS\Api\Exception( 'NO_TITLE', '1S303/9', 400 );
}
if ( !\IPS\Request::i()->description )
{
throw new \IPS\Api\Exception( 'NO_DESC', '1S303/A', 400 );
}
/* Validate files */
if ( !isset( \IPS\Request::i()->files ) or !is_array( \IPS\Request::i()->files ) or empty( \IPS\Request::i()->files ) )
{
throw new \IPS\Api\Exception( 'NO_FILES', '1L296/B', 400 );
}
if ( $this->member )
{
$this->_validateFilesForMember( $category );
}
/* Create file record */
$file = $this->_create( $category, $author );
/* Save records */
foreach ( array_keys( \IPS\Request::i()->files ) as $name )
{
$fileObject = \IPS\File::create( 'downloads_Files', $name, $_POST['files'][ $name ] );
\IPS\Db::i()->insert( 'downloads_files_records', array(
'record_file_id' => $file->id,
'record_type' => 'upload',
'record_location' => (string) $fileObject,
'record_realname' => $fileObject->originalFilename,
'record_size' => $fileObject->filesize(),
'record_time' => time(),
) );
}
if ( $category->bitoptions['allowss'] and isset( \IPS\Request::i()->screenshots ) )
{
$primary = 1;
foreach ( array_keys( \IPS\Request::i()->screenshots ) as $name )
{
$fileObject = \IPS\File::create( 'downloads_Screenshots', $name, $_POST['screenshots'][ $name ] );
\IPS\Db::i()->insert( 'downloads_files_records', array(
'record_file_id' => $file->id,
'record_type' => 'ssupload',
'record_location' => (string) $fileObject,
'record_thumb' => (string) $fileObject->thumbnail( 'downloads_Screenshots' ),
'record_realname' => $fileObject->originalFilename,
'record_size' => \strlen( $fileObject->contents() ),
'record_time' => time(),
'record_no_watermark' => NULL,
'record_default' => $primary
) );
$primary = 0;
}
}
/* Recaluclate properties */
$file = $this->_recalculate( $file );
/* Return */
$file->save();
return new \IPS\Api\Response( 201, $file->apiOutput( $this->member ) );
}
/**
* Validate that the authorized member can upload the files provided
*
* @param \IPS\downloads\Category $category The category
* @return void
*/
protected function _validateFilesForMember( $category )
{
foreach ( \IPS\Request::i()->files as $name => $content )
{
if ( $category->types )
{
$ext = mb_substr( $name, mb_strrpos( $name, '.' ) + 1 );
if( !in_array( mb_strtolower( $ext ), array_map( 'mb_strtolower', $category->types ) ) )
{
throw new \IPS\Api\Exception( 'BAD_FILE_EXT', '1S303/I', 400 );
}
}
if ( $category->maxfile and \strlen( $content ) > ( $category->maxfile * 1024 ) )
{
throw new \IPS\Api\Exception( 'BAD_FILE_SIZE', '1S303/J', 400 );
}
}
if ( $category->bitoptions['allowss'] )
{
if ( isset( \IPS\Request::i()->screenshots ) and \IPS\Request::i()->screenshots )
{
foreach ( \IPS\Request::i()->screenshots as $name => $content )
{
if ( $category->maxss and \strlen( $content ) > ( $category->maxss * 1024 ) )
{
throw new \IPS\Api\Exception( 'BAD_SS_SIZE', '1S303/L', 400 );
}
try
{
$image = \IPS\Image::create( $content );
if ( $category->maxdims )
{
$maxDims = explode( 'x', $category->maxdims );
if ( $image->width > $maxDims[0] or $image->height > $maxDims[1] )
{
throw new \IPS\Api\Exception( 'BAD_SS_DIMS', '1S303/M', 400 );
}
}
}
catch ( \InvalidArgumentException $e )
{
throw new \IPS\Api\Exception( 'BAD_SS', '1S303/K', 400 );
}
}
}
elseif ( $category->bitoptions['reqss'] )
{
throw new \IPS\Api\Exception( 'NO_SS', '1S303/N', 400 );
}
}
}
/**
* POST /downloads/files/{id}
* Edit a file
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authenticated user has permission to lock records).
* @apiparam int category The ID number of the calendar the file should be created in
* @apiparam int author The ID number of the member creating the file (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string title The file name
* @apiparam string description The description as HTML (e.g. "<p>This is an file.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the file post date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the file. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the file should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int featured 1/0 indicating if the file should be featured
* @throws 2S303/C INVALID_ID The file ID is invalid or the authorized user does not have permission to view it
* @throws 1S303/D NO_CATEGORY The category ID does not exist or the authorized user does not have permission to post in it
* @throws 1S303/E NO_AUTHOR The author ID does not exist
* @throws 2S303/O NO_PERMISSION The authorized user does not have permission to edit the file
* @return \IPS\downloads\File
*/
public function POSTitem( $id )
{
try
{
$file = \IPS\downloads\File::load( $id );
if ( $this->member and !$file->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$file->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2S303/O', 403 );
}
/* New category */
if ( isset( \IPS\Request::i()->category ) and \IPS\Request::i()->category != $file->category_id and ( !$this->member or $file->canMove( $this->member ) ) )
{
try
{
$newCategory = \IPS\downloads\Category::load( \IPS\Request::i()->category );
if ( $this->member and !$newCategory->can( 'add', $this->member ) )
{
throw new \OutOfRangeException;
}
$file->move( $newCategory );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_CATEGORY', '1S303/D', 400 );
}
}
/* New author */
if ( !$this->member and isset( \IPS\Request::i()->author ) )
{
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$member->member_id )
{
throw new \OutOfRangeException;
}
$file->changeAuthor( $member );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S303/E', 400 );
}
}
/* Everything else */
$this->_createOrUpdate( $file, 'edit' );
/* Save and return */
$file->save();
return new \IPS\Api\Response( 200, $file->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/C', 404 );
}
}
/**
* Create or update file
*
* @param \IPS\Content\Item $item The item
* @param string $type add or edit
* @return \IPS\Content\Item
*/
protected function _createOrUpdate( \IPS\Content\Item $item, $type='add' )
{
/* Description */
if ( isset( \IPS\Request::i()->description ) )
{
$descriptionContents = \IPS\Request::i()->description;
if ( $this->member )
{
$descriptionContents = \IPS\Text\Parser::parseStatic( $descriptionContents, TRUE, NULL, $this->member, 'downloads_Downloads' );
}
$item->desc = $descriptionContents;
}
/* Version */
if ( isset( \IPS\Request::i()->version ) )
{
$item->version = \IPS\Request::i()->version;
}
/* Changelog */
if ( isset( \IPS\Request::i()->changelog ) )
{
$item->changelog = \IPS\Request::i()->changelog;
}
$file = parent::_createOrUpdate( $item, $type );
if ( \IPS\Application::appIsEnabled('forums') and $file->container()->forum_id and !$file->hidden() )
{
$file->syncTopic();
}
return $file;
}
/**
* Recalculate stored properties
*
* @param \IPS\downloads\File $file The file
* @return \IPS\downloads\File
*/
protected function _recalculate( $file )
{
/* File size */
$file->size = floatval( \IPS\Db::i()->select( 'SUM(record_size)', 'downloads_files_records', array( 'record_file_id=? AND record_type=? AND record_backup=0', $file->id, 'upload' ) )->first() );
/* Work out the new primary screenshot */
try
{
$file->primary_screenshot = \IPS\Db::i()->select( 'record_id', 'downloads_files_records', array( 'record_file_id=? AND ( record_type=? OR record_type=? ) AND record_backup=0', $file->id, 'ssupload', 'sslink' ), 'record_default DESC, record_id ASC' )->first();
}
catch ( \UnderflowException $e ) { }
/* Return */
return $file;
}
/**
* GET /downloads/files/{id}/comments
* Get comments on an file
*
* @param int $id ID Number
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2S303/2 INVALID_ID The file ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\downloads\File\Comment>
*/
public function GETitem_comments( $id )
{
try
{
return $this->_comments( $id, 'IPS\downloads\File\Comment' );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/2', 404 );
}
}
/**
* GET /downloads/files/{id}/reviews
* Get reviews on an file
*
* @param int $id ID Number
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2S303/3 INVALID_ID The file ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\downloads\File\Review>
*/
public function GETitem_reviews( $id )
{
try
{
return $this->_comments( $id, 'IPS\downloads\File\Review' );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/3', 404 );
}
}
/**
* GET /downloads/files/{id}/history
* Get previous versions for a file
*
* @param int $id ID Number
* @throws 2S303/4 INVALID_ID The file ID does not exist or the authorized user does not have permission to view it
* @return array
* @apiresponse int id The version ID number (use to get more information about this version in GET /downloads/files/{id})
* @apiresponse string version The version number provided by the user
* @apiresponse string changelog What was new in this version
* @apiresponse bool hidden If this version is hidden
*/
public function GETitem_history( $id )
{
try
{
$file = \IPS\downloads\File::load( $id );
if ( $this->member and !$file->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
$versions = array();
foreach ( \IPS\Db::i()->select( '*', 'downloads_filebackup', array( 'b_fileid=?', $id ), 'b_backup DESC' ) as $backup )
{
$versions[] = array(
'id' => $backup['b_id'],
'version' => $backup['b_version'],
'changelog' => $backup['b_changelog'],
'hidden' => (bool) $backup['b_hidden'],
);
}
return new \IPS\Api\Response( 200, $versions );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/4', 404 );
}
}
/**
* POST /downloads/files/{id}/history
* Upload a new file version
*
* @apiparam string title The file name
* @apiparam string description The description as HTML (e.g. "<p>This is an file.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam string version The version number
* @apiparam string changelog What changed in this version
* @apiparam int save If 1 this will be saved as a new version and the previous version available in the history. If 0, will simply replace the existing files/screenshots. Defaults to 1. Ignored if category does not have versioning enabled or authorized user does not have permission to disable.
* @reqapiparam object files Files. Keys should be filename (e.g. 'file.txt') and values should be file content - will replace all current files
* @apiparam object screenshots Screenshots. Keys should be filename (e.g. 'screenshot1.png') and values should be file content - will replace all current screenshots
* @throws 2S303/F INVALID_ID The file ID is invalid or the authorized user does not have permission to view it
* @throws 1S303/G NO_FILES No files were supplied
* @throws 2S303/Q NO_PERMISSION The authorized user does not have permission to edit the file
* @throws 1S303/I BAD_FILE_EXT One of the files has a file type that is not allowed
* @throws 1S303/J BAD_FILE_SIZE One of the files is too big
* @throws 1S303/K BAD_SS One of the screenshots is not a valid image
* @throws 1S303/L BAD_SS_SIZE One of the screenshots is too big by filesize
* @throws 1S303/M BAD_SS_DIMS One of the screenshots is too big by dimensions
* @throws 1S303/N NO_SS No screenshots are provided, but screenshots are required for the category
* @return \IPS\downloads\File
*/
public function POSTitem_history( $id )
{
try
{
/* Load file */
$file = \IPS\downloads\File::load( $id );
if ( $this->member and !$file->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$file->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2S303/O', 403 );
}
$category = $file->container();
/* Validate files */
if ( !isset( \IPS\Request::i()->files ) or !is_array( \IPS\Request::i()->files ) or empty( \IPS\Request::i()->files ) )
{
throw new \IPS\Api\Exception( 'NO_FILES', '1L296/B', 400 );
}
if ( $this->member )
{
$this->_validateFilesForMember( $category );
}
/* Save current version? */
$save = FALSE;
if ( $category->versioning !== 0 )
{
if ( $this->member and !$this->member->group['idm_bypass_revision'] )
{
$save = TRUE;
}
else
{
$save = isset( \IPS\Request::i()->save ) ? ( (bool) \IPS\Request::i()->save ) : TRUE;
}
}
if ( $save )
{
$file->saveVersion();
}
else
{
foreach ( \IPS\Db::i()->select( 'record_location', 'downloads_files_records', array( 'record_file_id=?', $file->id ) ) as $record )
{
if ( in_array( $record['record_type'], array( 'upload', 'ssupload' ) ) )
{
try
{
\IPS\File::get( $record['record_type'] == 'upload' ? 'downloads_Files' : 'downloads_Screenshots', $url )->delete();
}
catch ( \Exception $e ) { }
}
}
\IPS\Db::i()->delete( 'downloads_files_records', array( 'record_file_id=?', $file->id ) );
}
/* Insert the new records */
foreach ( array_keys( \IPS\Request::i()->files ) as $name )
{
$fileObject = \IPS\File::create( 'downloads_Files', $name, $_POST['files'][ $name ] );
\IPS\Db::i()->insert( 'downloads_files_records', array(
'record_file_id' => $file->id,
'record_type' => 'upload',
'record_location' => (string) $fileObject,
'record_realname' => $fileObject->originalFilename,
'record_size' => $fileObject->filesize(),
'record_time' => time(),
) );
}
if ( isset( \IPS\Request::i()->screenshots ) )
{
$primary = 1;
foreach ( array_keys( \IPS\Request::i()->screenshots ) as $name )
{
$fileObject = \IPS\File::create( 'downloads_Screenshots', $name, $_POST['screenshots'][ $name ] );
\IPS\Db::i()->insert( 'downloads_files_records', array(
'record_file_id' => $file->id,
'record_type' => 'ssupload',
'record_location' => (string) $fileObject,
'record_thumb' => (string) $fileObject->thumbnail( 'downloads_Screenshots' ),
'record_realname' => $fileObject->originalFilename,
'record_size' => \strlen( $fileObject->contents() ),
'record_time' => time(),
'record_no_watermark' => NULL,
'record_default' => $primary
) );
$primary = 0;
}
}
/* Update */
$file = $this->_createOrUpdate( $file, 'edit' );
$file = $this->_recalculate( $file );
/* Save */
$file->updated = time();
$file->save();
/* Send notifications */
if ( $file->open )
{
$file->sendUpdateNotifications();
}
/* Return */
return new \IPS\Api\Response( 200, $file->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/F', 404 );
}
}
/**
* DELETE /downloads/files/{id}
* Delete a file
*
* @param int $id ID Number
* @throws 2S303/5 INVALID_ID The file ID does not exist
* @throws 2S303/P NO_PERMISSION The authorized user does not have permission to delete the file
* @return void
*/
public function DELETEitem( $id )
{
try
{
$item = \IPS\downloads\File::load( $id );
if ( $this->member and !$item->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2G316/G', 404 );
}
$item->delete();
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2S303/P', 404 );
}
}
}