2016-10-02 21:07:17 +00:00
< ? php
2016-10-06 02:08:43 +00:00
declare ( strict_types = 1 );
2017-03-28 04:12:12 +00:00
namespace JKingWeb\Arsse ;
2017-02-20 22:04:13 +00:00
use PasswordGenerator\Generator as PassGen ;
2016-10-02 21:07:17 +00:00
class Database {
2017-02-19 22:02:03 +00:00
2017-02-16 20:29:42 +00:00
const SCHEMA_VERSION = 1 ;
const FORMAT_TS = " Y-m-d h:i:s " ;
const FORMAT_DATE = " Y-m-d " ;
const FORMAT_TIME = " h:i:s " ;
2017-02-19 22:02:03 +00:00
2017-02-16 20:29:42 +00:00
protected $data ;
public $db ;
2017-02-19 22:02:03 +00:00
private $driver ;
2016-10-15 13:45:23 +00:00
2017-04-06 15:02:47 +00:00
protected function generateSet ( array $props , array $valid ) : array {
2017-04-01 14:27:26 +00:00
$out = [
2017-04-06 15:02:47 +00:00
'setValues' => [],
'setTypes' => [],
'set' => [],
2017-04-01 14:27:26 +00:00
];
foreach ( $valid as $prop => $type ) {
if ( ! array_key_exists ( $prop , $props )) continue ;
2017-04-06 15:02:47 +00:00
$out [ 'setValues' ][] = $props [ $prop ];
$out [ 'setTypes' ][] = $type ;
2017-04-01 14:27:26 +00:00
$out [ 'set' ][] = " $prop = ? " ;
}
$out [ 'set' ] = implode ( " , " , $out [ 'set' ]);
return $out ;
2017-02-16 20:29:42 +00:00
}
2016-10-02 21:07:17 +00:00
2017-03-30 03:41:05 +00:00
public function __construct ( Db\Driver $db = null ) {
// if we're fed a pre-prepared driver, use it'
if ( $db ) {
$this -> db = $db ;
} else {
$this -> driver = $driver = Data :: $conf -> dbDriver ;
$this -> db = new $driver ( INSTALL );
$ver = $this -> db -> schemaVersion ();
if ( ! INSTALL && $ver < self :: SCHEMA_VERSION ) {
$this -> db -> schemaUpdate ( self :: SCHEMA_VERSION );
}
2017-02-16 20:29:42 +00:00
}
}
2016-10-02 21:07:17 +00:00
2017-02-16 20:29:42 +00:00
static public function listDrivers () : array {
$sep = \DIRECTORY_SEPARATOR ;
$path = __DIR__ . $sep . " Db " . $sep ;
$classes = [];
2017-03-07 23:01:13 +00:00
foreach ( glob ( $path . " * " . $sep . " Driver.php " ) as $file ) {
$name = basename ( dirname ( $file ));
$class = NS_BASE . " Db \\ $name\\Driver " ;
$classes [ $class ] = $class :: driverName ();
2017-02-16 20:29:42 +00:00
}
return $classes ;
}
2016-10-06 02:08:43 +00:00
2017-02-16 20:29:42 +00:00
public function schemaVersion () : int {
return $this -> db -> schemaVersion ();
}
2016-10-15 13:45:23 +00:00
2017-03-03 01:47:00 +00:00
public function schemaUpdate () : bool {
2017-03-03 03:43:59 +00:00
if ( $this -> db -> schemaVersion () < self :: SCHEMA_VERSION ) return $this -> db -> schemaUpdate ( self :: SCHEMA_VERSION );
2017-02-16 20:29:42 +00:00
return false ;
}
2016-10-18 15:42:21 +00:00
2017-02-16 20:29:42 +00:00
public function settingGet ( string $key ) {
2017-03-28 04:12:12 +00:00
$row = $this -> db -> prepare ( " SELECT value, type from arsse_settings where key = ? " , " str " ) -> run ( $key ) -> getRow ();
2017-02-16 20:29:42 +00:00
if ( ! $row ) return null ;
switch ( $row [ 'type' ]) {
case " int " : return ( int ) $row [ 'value' ];
case " numeric " : return ( float ) $row [ 'value' ];
case " text " : return $row [ 'value' ];
case " json " : return json_decode ( $row [ 'value' ]);
case " timestamp " : return date_create_from_format ( " ! " . self :: FORMAT_TS , $row [ 'value' ], new DateTimeZone ( " UTC " ));
case " date " : return date_create_from_format ( " ! " . self :: FORMAT_DATE , $row [ 'value' ], new DateTimeZone ( " UTC " ));
case " time " : return date_create_from_format ( " ! " . self :: FORMAT_TIME , $row [ 'value' ], new DateTimeZone ( " UTC " ));
case " bool " : return ( bool ) $row [ 'value' ];
case " null " : return null ;
default : return $row [ 'value' ];
}
}
2016-10-17 20:49:39 +00:00
2017-02-16 20:29:42 +00:00
public function settingSet ( string $key , $in , string $type = null ) : bool {
if ( ! $type ) {
switch ( gettype ( $in )) {
case " boolean " : $type = " bool " ; break ;
case " integer " : $type = " int " ; break ;
2017-02-19 22:02:03 +00:00
case " double " : $type = " numeric " ; break ;
2017-02-16 20:29:42 +00:00
case " string " :
2017-02-19 22:02:03 +00:00
case " array " : $type = " json " ; break ;
2017-02-16 20:29:42 +00:00
case " resource " :
case " unknown type " :
2017-02-19 22:02:03 +00:00
case " NULL " : $type = " null " ; break ;
2017-02-16 20:29:42 +00:00
case " object " :
if ( $in instanceof DateTimeInterface ) {
$type = " timestamp " ;
} else {
$type = " text " ;
}
break ;
2017-02-19 22:02:03 +00:00
default : $type = 'null' ; break ;
2017-02-16 20:29:42 +00:00
}
}
$type = strtolower ( $type );
switch ( $type ) {
case " integer " :
$type = " int " ;
case " int " :
2017-03-09 22:14:26 +00:00
$value = $in ;
2017-02-16 20:29:42 +00:00
break ;
case " float " :
case " double " :
case " real " :
$type = " numeric " ;
case " numeric " :
2017-03-09 22:14:26 +00:00
$value = $in ;
2017-02-16 20:29:42 +00:00
break ;
case " str " :
case " string " :
$type = " text " ;
case " text " :
2017-03-09 22:14:26 +00:00
$value = $in ;
2017-02-16 20:29:42 +00:00
break ;
case " json " :
if ( is_array ( $in ) || is_object ( $in )) {
$value = json_encode ( $in );
} else {
2017-03-09 22:14:26 +00:00
$value = $in ;
2017-02-19 22:02:03 +00:00
}
2017-02-16 20:29:42 +00:00
break ;
case " datetime " :
$type = " timestamp " ;
case " timestamp " :
if ( $in instanceof DateTimeInterface ) {
$value = gmdate ( self :: FORMAT_TS , $in -> format ( " U " ));
} else if ( is_numeric ( $in )) {
$value = gmdate ( self :: FORMAT_TS , $in );
} else {
$value = gmdate ( self :: FORMAT_TS , gmstrftime ( $in ));
}
break ;
case " date " :
if ( $in instanceof DateTimeInterface ) {
$value = gmdate ( self :: FORMAT_DATE , $in -> format ( " U " ));
} else if ( is_numeric ( $in )) {
$value = gmdate ( self :: FORMAT_DATE , $in );
} else {
$value = gmdate ( self :: FORMAT_DATE , gmstrftime ( $in ));
}
break ;
case " time " :
if ( $in instanceof DateTimeInterface ) {
$value = gmdate ( self :: FORMAT_TIME , $in -> format ( " U " ));
} else if ( is_numeric ( $in )) {
$value = gmdate ( self :: FORMAT_TIME , $in );
} else {
$value = gmdate ( self :: FORMAT_TIME , gmstrftime ( $in ));
}
break ;
case " boolean " :
case " bit " :
$type = " bool " ;
case " bool " :
$value = ( int ) $in ;
break ;
case " null " :
$value = null ;
break ;
default :
$type = " text " ;
2017-03-09 22:14:26 +00:00
$value = $in ;
2017-02-16 20:29:42 +00:00
break ;
}
2017-03-28 04:12:12 +00:00
return ( bool ) $this -> db -> prepare ( " REPLACE INTO arsse_settings(key,value,type) values(?,?,?) " , " str " , " str " , " str " ) -> run ( $key , $value , $type ) -> changes ();
2017-02-16 20:29:42 +00:00
}
2016-10-17 20:49:39 +00:00
2017-02-16 20:29:42 +00:00
public function settingRemove ( string $key ) : bool {
2017-03-28 04:12:12 +00:00
$this -> db -> prepare ( " DELETE from arsse_settings where key is ? " , " str " ) -> run ( $key );
2017-02-16 20:29:42 +00:00
return true ;
}
2016-10-17 20:49:39 +00:00
2017-02-16 20:29:42 +00:00
public function userExists ( string $user ) : bool {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
return ( bool ) $this -> db -> prepare ( " SELECT count(*) from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 20:29:42 +00:00
}
2016-10-18 15:42:21 +00:00
2017-02-20 22:04:13 +00:00
public function userAdd ( string $user , string $password = null ) : string {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 22:04:13 +00:00
if ( $this -> userExists ( $user )) throw new User\Exception ( " alreadyExists " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 22:50:00 +00:00
if ( $password === null ) $password = ( new PassGen ) -> length ( Data :: $conf -> userTempPasswordLength ) -> get ();
2017-02-20 22:04:13 +00:00
$hash = " " ;
if ( strlen ( $password ) > 0 ) $hash = password_hash ( $password , \PASSWORD_DEFAULT );
2017-03-28 04:12:12 +00:00
$this -> db -> prepare ( " INSERT INTO arsse_users(id,password) values(?,?) " , " str " , " str " ) -> runArray ([ $user , $hash ]);
2017-02-20 22:04:13 +00:00
return $password ;
2017-02-16 20:29:42 +00:00
}
2016-10-28 12:27:35 +00:00
2017-02-16 20:29:42 +00:00
public function userRemove ( string $user ) : bool {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
if ( $this -> db -> prepare ( " DELETE from arsse_users where id is ? " , " str " ) -> run ( $user ) -> changes () < 1 ) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-16 20:29:42 +00:00
return true ;
}
2016-10-28 12:27:35 +00:00
2017-02-16 20:29:42 +00:00
public function userList ( string $domain = null ) : array {
2017-03-30 03:41:05 +00:00
$out = [];
2017-02-16 20:29:42 +00:00
if ( $domain !== null ) {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( " @ " . $domain , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $domain ]);
2017-02-16 20:29:42 +00:00
$domain = str_replace ([ " \\ " , " % " , " _ " ],[ " \\ \\ " , " \\ % " , " \\ _ " ], $domain );
$domain = " %@ " . $domain ;
2017-03-30 03:41:05 +00:00
foreach ( $this -> db -> prepare ( " SELECT id from arsse_users where id like ? " , " str " ) -> run ( $domain ) as $user ) {
$out [] = $user [ 'id' ];
}
2017-02-16 20:29:42 +00:00
} else {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( " " , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => " global " ]);
2017-03-30 03:41:05 +00:00
foreach ( $this -> db -> prepare ( " SELECT id from arsse_users " ) -> run () as $user ) {
$out [] = $user [ 'id' ];
}
2017-02-16 20:29:42 +00:00
}
2017-03-30 03:41:05 +00:00
return $out ;
2017-02-16 20:29:42 +00:00
}
2017-02-19 22:02:03 +00:00
2017-02-16 20:29:42 +00:00
public function userPasswordGet ( string $user ) : string {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 22:04:13 +00:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
return ( string ) $this -> db -> prepare ( " SELECT password from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 20:29:42 +00:00
}
2017-02-19 22:02:03 +00:00
2017-02-20 22:04:13 +00:00
public function userPasswordSet ( string $user , string $password = null ) : string {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 22:04:13 +00:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 22:50:00 +00:00
if ( $password === null ) $password = ( new PassGen ) -> length ( Data :: $conf -> userTempPasswordLength ) -> get ();
2017-02-20 22:04:13 +00:00
$hash = " " ;
2017-03-30 03:41:05 +00:00
if ( strlen ( $password ) > 0 ) $hash = password_hash ( $password , \PASSWORD_DEFAULT );
2017-03-28 04:12:12 +00:00
$this -> db -> prepare ( " UPDATE arsse_users set password = ? where id is ? " , " str " , " str " ) -> run ( $hash , $user );
2017-02-20 22:04:13 +00:00
return $password ;
2017-02-16 20:29:42 +00:00
}
2016-10-28 12:27:35 +00:00
2017-02-16 20:29:42 +00:00
public function userPropertiesGet ( string $user ) : array {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
$prop = $this -> db -> prepare ( " SELECT name,rights from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getRow ();
2017-03-30 03:41:05 +00:00
if ( ! $prop ) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-16 20:29:42 +00:00
return $prop ;
}
2016-10-28 12:27:35 +00:00
2017-03-30 03:41:05 +00:00
public function userPropertiesSet ( string $user , array $properties ) : array {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-04-01 14:27:26 +00:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-16 20:29:42 +00:00
$valid = [ // FIXME: add future properties
2017-02-19 22:02:03 +00:00
" name " => " str " ,
2017-02-16 20:29:42 +00:00
];
2017-04-06 15:02:47 +00:00
$data = $this -> generateSet ( $properties , $valid );
2017-04-01 14:27:26 +00:00
extract ( $data );
2017-04-06 15:02:47 +00:00
$this -> db -> prepare ( " UPDATE arsse_users set $set where id is ? " , $setTypes , " str " ) -> run ( $setValues , $user );
2017-02-16 20:29:42 +00:00
return $this -> userPropertiesGet ( $user );
}
2016-11-04 02:54:27 +00:00
2017-02-16 20:29:42 +00:00
public function userRightsGet ( string $user ) : int {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
return ( int ) $this -> db -> prepare ( " SELECT rights from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 20:29:42 +00:00
}
2016-11-04 02:54:27 +00:00
2017-02-16 20:29:42 +00:00
public function userRightsSet ( string $user , int $rights ) : bool {
2017-03-28 22:50:00 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ , $rights )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-30 03:41:05 +00:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 04:12:12 +00:00
$this -> db -> prepare ( " UPDATE arsse_users set rights = ? where id is ? " , " int " , " str " ) -> run ( $rights , $user );
2017-02-16 20:29:42 +00:00
return true ;
}
2016-10-28 12:27:35 +00:00
2017-03-07 23:01:13 +00:00
public function folderAdd ( string $user , array $data ) : int {
// If the user isn't authorized to perform this action then throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) {
2017-03-07 23:01:13 +00:00
throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
}
// If the user doesn't exist throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! $this -> userExists ( $user )) {
2017-03-07 23:01:13 +00:00
throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
}
// if the desired folder name is missing or invalid, throw an exception
2017-03-31 19:27:59 +00:00
if ( ! array_key_exists ( " name " , $data ) || $data [ 'name' ] == " " ) {
2017-03-07 23:01:13 +00:00
throw new Db\ExceptionInput ( " missing " , [ " action " => __FUNCTION__ , " field " => " name " ]);
} else if ( ! strlen ( trim ( $data [ 'name' ]))) {
throw new Db\ExceptionInput ( " whitespace " , [ " action " => __FUNCTION__ , " field " => " name " ]);
}
// normalize folder's parent, if there is one
$parent = array_key_exists ( " parent " , $data ) ? ( int ) $data [ 'parent' ] : 0 ;
if ( $parent === 0 ) {
// if no parent is specified, do nothing
$parent = null ;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
2017-04-01 18:49:31 +00:00
$p = $this -> db -> prepare ( " SELECT id from arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $parent ) -> getValue ();
if ( ! $p ) throw new Db\ExceptionInput ( " idMissing " , [ " action " => __FUNCTION__ , " field " => " parent " , 'id' => $parent ]);
2017-03-07 23:01:13 +00:00
}
2017-03-10 03:41:11 +00:00
// check if a folder by the same name already exists, because nulls are wonky in SQL
// FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion?
2017-03-28 04:12:12 +00:00
if ( $this -> db -> prepare ( " SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ? " , " str " , " int " , " str " ) -> run ( $user , $parent , $data [ 'name' ]) -> getValue () > 0 ) {
2017-03-10 03:41:11 +00:00
throw new Db\ExceptionInput ( " constraintViolation " ); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
2017-04-01 18:49:31 +00:00
return $this -> db -> prepare ( " INSERT INTO arsse_folders(owner,parent,name) values(?,?,?) " , " str " , " int " , " str " ) -> run ( $user , $parent , $data [ 'name' ]) -> lastId ();
2017-03-07 23:01:13 +00:00
}
2017-03-25 02:39:18 +00:00
public function folderList ( string $user , int $parent = null , bool $recursive = true ) : Db\Result {
// if the user isn't authorized to perform this action then throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) {
2017-03-25 02:39:18 +00:00
throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
}
// if the user doesn't exist throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! $this -> userExists ( $user )) {
2017-03-25 02:39:18 +00:00
throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
}
2017-03-31 21:42:28 +00:00
// check to make sure the parent exists, if one is specified
if ( ! is_null ( $parent )) {
if ( ! $this -> db -> prepare ( " SELECT count(*) from arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $parent ) -> getValue ()) {
throw new Db\ExceptionInput ( " idMissing " , [ " action " => __FUNCTION__ , " field " => " parent " , 'id' => $parent ]);
}
}
2017-03-25 02:39:18 +00:00
// if we're not returning a recursive list we can use a simpler query
if ( ! $recursive ) {
2017-03-31 21:42:28 +00:00
return $this -> db -> prepare ( " SELECT id,name,parent from arsse_folders where owner is ? and parent is ? " , " str " , " int " ) -> run ( $user , $parent );
2017-03-25 02:39:18 +00:00
} else {
return $this -> db -> prepare (
2017-03-28 04:12:12 +00:00
" WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and parent is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) " .
" SELECT id,name,parent from arsse_folders where id in(SELECT id from folders) order by name " ,
2017-03-27 12:39:24 +00:00
" str " , " int " ) -> run ( $user , $parent );
2017-03-25 02:39:18 +00:00
}
}
2017-03-26 20:16:15 +00:00
2017-03-31 22:48:24 +00:00
public function folderRemove ( string $user , int $id ) : bool {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-04-01 14:27:26 +00:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
$changes = $this -> db -> prepare ( " DELETE FROM arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> changes ();
if ( ! $changes ) throw new Db\ExceptionInput ( " idMissing " , [ " action " => __FUNCTION__ , " field " => " folder " , 'id' => $id ]);
return true ;
}
public function folderPropertiesGet ( string $user , int $id ) : array {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
$props = $this -> db -> prepare ( " SELECT id,name,parent from arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> getRow ();
if ( ! $props ) throw new Db\ExceptionInput ( " idMissing " , [ " action " => __FUNCTION__ , " field " => " folder " , 'id' => $id ]);
return $props ;
}
public function folderPropertiesSet ( string $user , int $id , array $data ) : bool {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
// layer the existing folder properties onto the new desired one
$data = array_merge ( $this -> folderPropertiesGet ( $user , $id ), $data );
// if the desired folder name is missing or invalid, throw an exception
if ( ! array_key_exists ( " name " , $data ) || $data [ 'name' ] == " " ) {
throw new Db\ExceptionInput ( " missing " , [ " action " => __FUNCTION__ , " field " => " name " ]);
} else if ( ! strlen ( trim ( $data [ 'name' ]))) {
throw new Db\ExceptionInput ( " whitespace " , [ " action " => __FUNCTION__ , " field " => " name " ]);
2017-03-31 22:48:24 +00:00
}
2017-04-01 14:27:26 +00:00
// normalize folder's parent, if there is one
$parent = array_key_exists ( " parent " , $data ) ? ( int ) $data [ 'parent' ] : 0 ;
if ( $parent === 0 ) {
// if no parent is specified, do nothing
$parent = null ;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
$p = $this -> db -> prepare (
" WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) " .
2017-04-01 18:49:31 +00:00
" SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ? " ,
2017-04-01 14:27:26 +00:00
" str " , " int " , " str " , " int " ) -> run ( $user , $id , $user , $parent ) -> getRow ();
if ( ! $p ) {
throw new Db\ExceptionInput ( " idMissing " , [ " action " => __FUNCTION__ , " field " => " parent " , 'id' => $parent ]);
} else {
2017-04-01 18:49:31 +00:00
// if using the desired parent would create a circular dependence, throw an exception
2017-04-01 14:27:26 +00:00
if ( ! $p [ 'valid' ]) throw new Db\ExceptionInput ( " circularDependence " , [ " action " => __FUNCTION__ , " field " => " parent " , 'id' => $parent ]);
}
}
$data [ 'parent' ] = $parent ;
// check to make sure the target folder name/location would not create a duplicate (we must di this check because null is not distinct in SQL)
$existing = $this -> db -> prepare ( " SELECT id from arsse_folders where owner is ? and parent is ? and name is ? " , " str " , " int " , " str " ) -> run ( $user , $data [ 'parent' ], $data [ 'name' ]) -> getValue ();
if ( ! is_null ( $existing ) && $existing != $id ) {
throw new Db\ExceptionInput ( " constraintViolation " ); // FIXME: There needs to be a practical message here
}
$valid = [
'name' => " str " ,
'parent' => " int " ,
];
2017-04-06 15:02:47 +00:00
$data = $this -> generateSet ( $data , $valid );
2017-04-01 14:27:26 +00:00
extract ( $data );
2017-04-06 15:02:47 +00:00
$this -> db -> prepare ( " UPDATE arsse_folders set $set where owner is ? and id is ? " , $setTypes , " str " , " int " ) -> run ( $setValues , $user , $id );
2017-04-01 14:27:26 +00:00
return true ;
2017-03-31 22:48:24 +00:00
}
public function subscriptionAdd ( string $user , string $url , string $fetchUser = " " , string $fetchPassword = " " ) : int {
// If the user isn't authorized to perform this action then throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) {
2017-03-31 22:48:24 +00:00
throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
}
// If the user doesn't exist throw an exception.
2017-04-01 14:27:26 +00:00
if ( ! $this -> userExists ( $user )) {
2017-03-31 22:48:24 +00:00
throw new User\Exception ( " doesNotExist " , [ " user " => $user , " action " => __FUNCTION__ ]);
}
$this -> db -> begin ();
// If the feed doesn't already exist in the database then add it to the database
// after determining its validity with PicoFeed.
$qFeed = $this -> db -> prepare ( " SELECT id from arsse_feeds where url is ? and username is ? and password is ? " , " str " , " str " , " str " );
$feed = $qFeed -> run ( $url , $fetchUser , $fetchPassword ) -> getValue ();
2017-04-01 14:27:26 +00:00
if ( $feed === null ) {
2017-03-31 22:48:24 +00:00
$feed = new Feed ( $url );
$feed -> parse ();
// Add the feed to the database and return its Id which will be used when adding
// its articles to the database.
$feedID = $this -> db -> prepare (
' INSERT INTO arsse_feeds ( url , title , favicon , source , updated , modified , etag , username , password )
values ( ? , ? , ? , ? , ? , ? , ? , ? , ? ) ' ,
'str' , 'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'str' , 'str' , 'str' ) -> run (
$url ,
$feed -> data -> title ,
// Grab the favicon for the feed; returns an empty string if it cannot find one.
$feed -> favicon ,
$feed -> data -> siteUrl ,
$feed -> data -> date ,
$feed -> resource -> getLastModified (),
$feed -> resource -> getEtag (),
$fetchUser ,
$fetchPassword
) -> lastId ();
// Add each of the articles to the database.
2017-04-01 14:27:26 +00:00
foreach ( $feed -> data -> items as $i ) {
2017-04-01 19:42:10 +00:00
$this -> articleAdd ( $feedID , $i );
2017-03-31 22:48:24 +00:00
}
}
// Add the feed to the user's subscriptions.
$sub = $this -> db -> prepare ( 'INSERT INTO arsse_subscriptions(owner,feed) values(?,?)' , 'str' , 'int' ) -> run ( $user , $feedID ) -> lastId ();
$this -> db -> commit ();
return $sub ;
}
public function subscriptionRemove ( string $user , int $id ) : bool {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
return ( bool ) $this -> db -> prepare ( " DELETE from arsse_subscriptions where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> changes ();
}
2017-04-01 19:42:10 +00:00
public function articleAdd ( int $feedID , \PicoFeed\Parser\Item $article ) : int {
2017-03-26 20:16:15 +00:00
$this -> db -> begin ();
2017-04-01 19:42:10 +00:00
$articleID = $this -> db -> prepare ( ' INSERT INTO arsse_articles ( feed , url , title , author , published , edited , guid , content , url_title_hash , url_content_hash , title_content_hash )
2017-03-26 20:16:15 +00:00
values ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) ' ,
'int' , 'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'str' , 'str' , 'str' , 'str' , 'str' ) -> run (
$feedID ,
$article -> url ,
$article -> title ,
$article -> author ,
$article -> publishedDate ,
$article -> updatedDate ,
$article -> id ,
$article -> content ,
$article -> urlTitleHash ,
$article -> urlContentHash ,
$article -> titleContentHash
) -> lastId ();
// If the article has categories add them into the categories database.
2017-04-01 19:42:10 +00:00
$this -> categoriesAdd ( $articleID , $article );
2017-03-30 14:42:37 +00:00
$this -> db -> commit ();
return 1 ;
}
2017-04-01 19:42:10 +00:00
public function categoriesAdd ( int $articleID , \PicoFeed\Parser\Item $article ) : int {
2017-03-30 14:42:37 +00:00
$this -> db -> begin ();
2017-03-26 20:16:15 +00:00
$categories = $article -> getTag ( 'category' );
2017-04-01 14:27:26 +00:00
if ( count ( $categories ) > 0 ) {
foreach ( $categories as $c ) {
2017-04-01 19:42:10 +00:00
$this -> db -> prepare ( 'INSERT INTO arsse_categories(article,name) values(?,?)' , 'int' , 'str' ) -> run ( $articleID , $c );
2017-03-26 20:16:15 +00:00
}
}
$this -> db -> commit ();
2017-04-01 19:42:10 +00:00
return count ( $categories );
2017-03-26 20:16:15 +00:00
}
public function updateFeeds () : int {
2017-03-28 04:12:12 +00:00
$feeds = $this -> db -> query ( 'SELECT id, url, username, password, DATEFORMAT("http", modified) AS lastmodified, etag FROM arsse_feeds' ) -> getAll ();
2017-04-01 14:27:26 +00:00
foreach ( $feeds as $f ) {
2017-03-30 14:42:37 +00:00
// Feed object throws an exception when there are problems, but that isn't ideal
// here. When an exception is occurred it should update the database with the
// error instead of failing.
try {
$feed = new Feed ( $f [ 'url' ], $f [ 'lastmodified' ], $f [ 'etag' ], $f [ 'username' ], $f [ 'password' ]);
} catch ( Feed\Exception $e ) {
2017-04-03 02:23:15 +00:00
$this -> db -> prepare ( 'UPDATE arsse_feeds SET err_count = err_count + 1, err_msg = ? WHERE id is ?' , 'str' , 'int' ) -> run (
2017-03-30 14:42:37 +00:00
$e -> getMessage (),
$f [ 'id' ]
);
continue ;
}
2017-03-26 20:16:15 +00:00
2017-03-30 14:42:37 +00:00
// If the feed has been updated then update the database.
2017-04-01 14:27:26 +00:00
if ( $feed -> resource -> isModified ()) {
2017-03-26 20:16:15 +00:00
$feed -> parse ();
$this -> db -> begin ();
2017-03-28 04:12:12 +00:00
$articles = $this -> db -> prepare ( 'SELECT id, url, title, author, DATEFORMAT("http", edited) AS edited_date, guid, content, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY id' , 'int' ) -> run ( $f [ 'id' ]) -> getAll ();
2017-03-26 20:16:15 +00:00
2017-04-01 14:27:26 +00:00
foreach ( $feed -> data -> items as $i ) {
2017-03-26 20:16:15 +00:00
// Iterate through the articles in the database to determine a match for the one
// in the just-parsed feed.
$match = null ;
2017-04-01 14:27:26 +00:00
foreach ( $articles as $a ) {
2017-03-26 20:16:15 +00:00
// If the id exists and is equal to one in the database then this is the post.
2017-04-01 14:27:26 +00:00
if ( $i -> id ) {
if ( $i -> id === $a [ 'guid' ]) {
2017-03-26 20:16:15 +00:00
$match = $a ;
}
}
// Otherwise if the id doesn't exist and any of the hashes match then this is
// the post.
2017-04-01 14:27:26 +00:00
elseif ( $i -> urlTitleHash === $a [ 'url_title_hash' ] || $i -> urlContentHash === $a [ 'url_content_hash' ] || $i -> titleContentHash === $a [ 'title_content_hash' ]) {
2017-03-26 20:16:15 +00:00
$match = $a ;
}
}
// If there is no match then this is a new post and must be added to the
// database.
2017-04-01 14:27:26 +00:00
if ( ! $match ) {
2017-03-26 20:16:15 +00:00
$this -> articleAdd ( $i );
continue ;
}
// With that out of the way determine if the post has been updated.
// If there is an updated date, and it doesn't match the database's then update
// the post.
$update = false ;
2017-04-01 14:27:26 +00:00
if ( $i -> updatedDate ) {
if ( $i -> updatedDate !== $match [ 'edited_date' ]) {
2017-03-26 20:16:15 +00:00
$update = true ;
}
}
// Otherwise if there isn't an updated date and any of the hashes don't match
// then update the post.
2017-04-01 14:27:26 +00:00
elseif ( $i -> urlTitleHash !== $match [ 'url_title_hash' ] || $i -> urlContentHash !== $match [ 'url_content_hash' ] || $i -> titleContentHash !== $match [ 'title_content_hash' ]) {
2017-03-26 20:16:15 +00:00
$update = true ;
}
2017-04-01 14:27:26 +00:00
if ( $update ) {
2017-03-28 04:12:12 +00:00
$this -> db -> prepare ( 'UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = ?, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id is ?' , 'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'datetime' , 'str' , 'str' , 'str' , 'str' , 'str' , 'int' ) -> run (
2017-03-26 20:16:15 +00:00
$i -> url ,
$i -> title ,
$i -> author ,
$i -> publishedDate ,
$i -> updatedDate ,
time (),
$i -> id ,
$i -> content ,
$i -> urlTitleHash ,
$i -> urlContentHash ,
$i -> titleContentHash ,
$match [ 'id' ]
);
2017-03-30 14:42:37 +00:00
// If the article has categories update them.
$this -> db -> prepare ( 'DELETE FROM arsse_categories WHERE article is ?' , 'int' ) -> run ( $match [ 'id' ]);
$this -> categoriesAdd ( $i , $match [ 'id' ]);
2017-03-26 20:16:15 +00:00
}
}
// Lastly update the feed database itself with updated information.
2017-03-30 14:42:37 +00:00
$this -> db -> prepare ( 'UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = ?, modified = ?, etag = ?, err_count = 0, err_msg = "" WHERE id is ?' , 'str' , 'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'str' , 'int' ) -> run (
2017-03-26 20:16:15 +00:00
$feed -> feedUrl ,
$feed -> title ,
$feed -> favicon ,
$feed -> siteUrl ,
$feed -> date ,
$feed -> resource -> getLastModified (),
$feed -> resource -> getEtag (),
$f [ 'id' ]
);
}
}
$this -> db -> commit ();
return 1 ;
}
2016-10-02 21:07:17 +00:00
}