2016-10-02 17:07:17 -04:00
< ? php
2016-10-05 22:08:43 -04:00
declare ( strict_types = 1 );
2017-03-28 00:12:12 -04:00
namespace JKingWeb\Arsse ;
2017-02-20 17:04:13 -05:00
use PasswordGenerator\Generator as PassGen ;
2017-06-18 10:23:37 -04:00
use JKingWeb\Arsse\Misc\Query ;
use JKingWeb\Arsse\Misc\Context ;
2016-10-02 17:07:17 -04:00
class Database {
2017-02-19 17:02:03 -05:00
2017-02-16 15:29:42 -05: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 17:02:03 -05:00
2017-02-16 15:29:42 -05:00
public $db ;
2017-05-18 13:21:17 -04:00
protected $dateFormatDefault = " sql " ;
2016-10-15 09:45:23 -04:00
2017-05-03 20:00:29 -04:00
public function __construct () {
2017-05-18 13:21:17 -04:00
$driver = Data :: $conf -> dbDriver ;
2017-05-03 20:00:29 -04:00
$this -> db = new $driver ( INSTALL );
$ver = $this -> db -> schemaVersion ();
if ( ! INSTALL && $ver < self :: SCHEMA_VERSION ) {
$this -> db -> schemaUpdate ( self :: SCHEMA_VERSION );
2017-02-16 15:29:42 -05:00
}
}
2016-10-02 17:07:17 -04:00
2017-05-18 13:21:17 -04:00
protected function caller () : string {
return debug_backtrace ( DEBUG_BACKTRACE_IGNORE_ARGS , 3 )[ 2 ][ 'function' ];
}
2017-05-18 23:03:33 -04:00
public function dateFormatDefault ( string $set = null ) : string {
2017-05-18 13:21:17 -04:00
if ( is_null ( $set )) return $this -> dateFormatDefault ;
$set = strtolower ( $set );
if ( in_array ( $set , [ " sql " , " iso8601 " , " unix " , " http " ])) {
$this -> dateFormatDefault = $set ;
}
return $this -> dateFormatDefault ;
}
2017-02-16 15:29:42 -05:00
static public function listDrivers () : array {
$sep = \DIRECTORY_SEPARATOR ;
$path = __DIR__ . $sep . " Db " . $sep ;
$classes = [];
2017-03-07 18:01:13 -05: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 15:29:42 -05:00
}
return $classes ;
}
2016-10-05 22:08:43 -04:00
2017-02-16 15:29:42 -05:00
public function schemaVersion () : int {
return $this -> db -> schemaVersion ();
}
2016-10-15 09:45:23 -04:00
2017-03-02 20:47:00 -05:00
public function schemaUpdate () : bool {
2017-03-02 22:43:59 -05:00
if ( $this -> db -> schemaVersion () < self :: SCHEMA_VERSION ) return $this -> db -> schemaUpdate ( self :: SCHEMA_VERSION );
2017-02-16 15:29:42 -05:00
return false ;
}
2016-10-18 11:42:21 -04:00
2017-04-20 21:59:12 -04:00
protected function generateSet ( array $props , array $valid ) : array {
$out = [
[], // query clause
[], // binding types
[], // binding values
];
foreach ( $valid as $prop => $type ) {
if ( ! array_key_exists ( $prop , $props )) continue ;
$out [ 0 ][] = " $prop = ? " ;
$out [ 1 ][] = $type ;
$out [ 2 ][] = $props [ $prop ];
}
$out [ 0 ] = implode ( " , " , $out [ 0 ]);
return $out ;
}
protected function generateIn ( array $values , string $type ) {
$out = [
[], // query clause
[], // binding types
];
// the query clause is just a series of question marks separated by commas
$out [ 0 ] = implode ( " , " , array_fill ( 0 , sizeof ( $values ), " ? " ));
// the binding types are just a repetition of the supplied type
$out [ 1 ] = array_fill ( 0 , sizeof ( $values ), $type );
return $out ;
}
2017-05-18 23:03:33 -04:00
public function begin () : Db\Transaction {
return $this -> db -> begin ();
}
2017-07-05 10:59:13 -04:00
public function settingGet ( string $key ) {
return $this -> db -> prepare ( " SELECT value from arsse_settings where key is ? " , " str " ) -> run ( $key ) -> getValue ();
}
2017-05-18 23:03:33 -04:00
2017-06-01 16:24:11 -04:00
public function settingSet ( string $key , string $value ) : bool {
$out = ! $this -> db -> prepare ( " UPDATE arsse_settings set value = ? where key is ? " , " str " , " str " ) -> run ( $value , $key ) -> changes ();
if ( ! $out ) {
$out = $this -> db -> prepare ( " INSERT INTO arsse_settings(key,value) " , " str " , " str " ) -> run ( $key , $value ) -> changes ();
2017-02-16 15:29:42 -05:00
}
2017-06-01 16:24:11 -04:00
return ( bool ) $out ;
2017-02-16 15:29:42 -05:00
}
2016-10-17 16:49:39 -04:00
2017-02-16 15:29:42 -05:00
public function settingRemove ( string $key ) : bool {
2017-03-28 00:12:12 -04:00
$this -> db -> prepare ( " DELETE from arsse_settings where key is ? " , " str " ) -> run ( $key );
2017-02-16 15:29:42 -05:00
return true ;
}
2016-10-17 16:49:39 -04:00
2017-02-16 15:29:42 -05:00
public function userExists ( string $user ) : bool {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04:00
return ( bool ) $this -> db -> prepare ( " SELECT count(*) from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 15:29:42 -05:00
}
2016-10-18 11:42:21 -04:00
2017-02-20 17:04:13 -05:00
public function userAdd ( string $user , string $password = null ) : string {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 17:04:13 -05:00
if ( $this -> userExists ( $user )) throw new User\Exception ( " alreadyExists " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 18:50:00 -04:00
if ( $password === null ) $password = ( new PassGen ) -> length ( Data :: $conf -> userTempPasswordLength ) -> get ();
2017-02-20 17:04:13 -05:00
$hash = " " ;
if ( strlen ( $password ) > 0 ) $hash = password_hash ( $password , \PASSWORD_DEFAULT );
2017-03-28 00:12:12 -04:00
$this -> db -> prepare ( " INSERT INTO arsse_users(id,password) values(?,?) " , " str " , " str " ) -> runArray ([ $user , $hash ]);
2017-02-20 17:04:13 -05:00
return $password ;
2017-02-16 15:29:42 -05:00
}
2016-10-28 08:27:35 -04:00
2017-02-16 15:29:42 -05:00
public function userRemove ( string $user ) : bool {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04: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 15:29:42 -05:00
return true ;
}
2016-10-28 08:27:35 -04:00
2017-02-16 15:29:42 -05:00
public function userList ( string $domain = null ) : array {
2017-03-29 23:41:05 -04:00
$out = [];
2017-02-16 15:29:42 -05:00
if ( $domain !== null ) {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( " @ " . $domain , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $domain ]);
2017-02-16 15:29:42 -05:00
$domain = str_replace ([ " \\ " , " % " , " _ " ],[ " \\ \\ " , " \\ % " , " \\ _ " ], $domain );
$domain = " %@ " . $domain ;
2017-03-29 23:41:05 -04:00
foreach ( $this -> db -> prepare ( " SELECT id from arsse_users where id like ? " , " str " ) -> run ( $domain ) as $user ) {
$out [] = $user [ 'id' ];
}
2017-02-16 15:29:42 -05:00
} else {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( " " , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => " global " ]);
2017-03-29 23:41:05 -04:00
foreach ( $this -> db -> prepare ( " SELECT id from arsse_users " ) -> run () as $user ) {
$out [] = $user [ 'id' ];
}
2017-02-16 15:29:42 -05:00
}
2017-03-29 23:41:05 -04:00
return $out ;
2017-02-16 15:29:42 -05:00
}
2017-02-19 17:02:03 -05:00
2017-02-16 15:29:42 -05:00
public function userPasswordGet ( string $user ) : string {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 17:04:13 -05:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04:00
return ( string ) $this -> db -> prepare ( " SELECT password from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 15:29:42 -05:00
}
2017-02-19 17:02:03 -05:00
2017-02-20 17:04:13 -05:00
public function userPasswordSet ( string $user , string $password = null ) : string {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-20 17:04:13 -05:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 18:50:00 -04:00
if ( $password === null ) $password = ( new PassGen ) -> length ( Data :: $conf -> userTempPasswordLength ) -> get ();
2017-02-20 17:04:13 -05:00
$hash = " " ;
2017-03-29 23:41:05 -04:00
if ( strlen ( $password ) > 0 ) $hash = password_hash ( $password , \PASSWORD_DEFAULT );
2017-03-28 00:12:12 -04:00
$this -> db -> prepare ( " UPDATE arsse_users set password = ? where id is ? " , " str " , " str " ) -> run ( $hash , $user );
2017-02-20 17:04:13 -05:00
return $password ;
2017-02-16 15:29:42 -05:00
}
2016-10-28 08:27:35 -04:00
2017-02-16 15:29:42 -05:00
public function userPropertiesGet ( string $user ) : array {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04:00
$prop = $this -> db -> prepare ( " SELECT name,rights from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getRow ();
2017-03-29 23:41:05 -04:00
if ( ! $prop ) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-16 15:29:42 -05:00
return $prop ;
}
2016-10-28 08:27:35 -04:00
2017-03-29 23:41:05 -04:00
public function userPropertiesSet ( string $user , array $properties ) : array {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-04-01 10:27:26 -04:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-02-16 15:29:42 -05:00
$valid = [ // FIXME: add future properties
2017-02-19 17:02:03 -05:00
" name " => " str " ,
2017-02-16 15:29:42 -05:00
];
2017-04-06 13:29:39 -04:00
list ( $setClause , $setTypes , $setValues ) = $this -> generateSet ( $properties , $valid );
$this -> db -> prepare ( " UPDATE arsse_users set $setClause where id is ? " , $setTypes , " str " ) -> run ( $setValues , $user );
2017-02-16 15:29:42 -05:00
return $this -> userPropertiesGet ( $user );
}
2016-11-03 22:54:27 -04:00
2017-02-16 15:29:42 -05:00
public function userRightsGet ( string $user ) : int {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04:00
return ( int ) $this -> db -> prepare ( " SELECT rights from arsse_users where id is ? " , " str " ) -> run ( $user ) -> getValue ();
2017-02-16 15:29:42 -05:00
}
2016-11-03 22:54:27 -04:00
2017-02-16 15:29:42 -05:00
public function userRightsSet ( string $user , int $rights ) : bool {
2017-03-28 18:50:00 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ , $rights )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-29 23:41:05 -04:00
if ( ! $this -> userExists ( $user )) throw new User\Exception ( " doesNotExist " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-03-28 00:12:12 -04:00
$this -> db -> prepare ( " UPDATE arsse_users set rights = ? where id is ? " , " int " , " str " ) -> run ( $rights , $user );
2017-02-16 15:29:42 -05:00
return true ;
}
2016-10-28 08:27:35 -04:00
2017-03-07 18:01:13 -05: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 10:27:26 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) {
2017-03-07 18:01:13 -05:00
throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
}
// if the desired folder name is missing or invalid, throw an exception
2017-03-31 15:27:59 -04:00
if ( ! array_key_exists ( " name " , $data ) || $data [ 'name' ] == " " ) {
2017-03-07 18:01:13 -05: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 14:49:31 -04: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 18:01:13 -05:00
}
2017-03-09 22:41:11 -05: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 00:12:12 -04: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-09 22:41:11 -05:00
throw new Db\ExceptionInput ( " constraintViolation " ); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
2017-04-01 14:49:31 -04: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 18:01:13 -05:00
}
2017-03-24 22:39:18 -04: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 10:27:26 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) {
2017-03-24 22:39:18 -04:00
throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
}
2017-03-31 17:42:28 -04: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-24 22:39:18 -04:00
// if we're not returning a recursive list we can use a simpler query
if ( ! $recursive ) {
2017-03-31 17:42:28 -04: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-24 22:39:18 -04:00
} else {
return $this -> db -> prepare (
2017-03-28 00:12:12 -04: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 08:39:24 -04:00
" str " , " int " ) -> run ( $user , $parent );
2017-03-24 22:39:18 -04:00
}
}
2017-03-26 16:16:15 -04:00
2017-03-31 18:48:24 -04: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 10:27:26 -04:00
$changes = $this -> db -> prepare ( " DELETE FROM arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> changes ();
2017-05-21 10:10:36 -04:00
if ( ! $changes ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " folder " , 'id' => $id ]);
2017-04-01 10:27:26 -04:00
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 ]);
$props = $this -> db -> prepare ( " SELECT id,name,parent from arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> getRow ();
2017-05-21 10:10:36 -04:00
if ( ! $props ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " folder " , 'id' => $id ]);
2017-04-01 10:27:26 -04:00
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 ]);
2017-05-18 13:21:17 -04:00
// validate the folder ID and, if specified, the parent to move it to
2017-05-21 10:10:36 -04:00
$parent = null ;
if ( array_key_exists ( " parent " , $data )) $parent = $data [ 'parent' ];
$f = $this -> folderValidateId ( $user , $id , $parent , true );
2017-05-18 13:21:17 -04:00
// if a new name is specified, validate it
if ( array_key_exists ( " name " , $data )) {
$this -> folderValidateName ( $data [ 'name' ]);
}
$data = array_merge ( $f , $data );
2017-04-09 18:15:00 -04:00
// check to make sure the target folder name/location would not create a duplicate (we must do this check because null is not distinct in SQL)
2017-04-01 10:27:26 -04:00
$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 13:29:39 -04:00
list ( $setClause , $setTypes , $setValues ) = $this -> generateSet ( $data , $valid );
2017-05-04 19:12:33 -04:00
return ( bool ) $this -> db -> prepare ( " UPDATE arsse_folders set $setClause where owner is ? and id is ? " , $setTypes , " str " , " int " ) -> run ( $setValues , $user , $id ) -> changes ();
2017-03-31 18:48:24 -04:00
}
2017-05-21 10:10:36 -04:00
protected function folderValidateId ( string $user , int $id = null , int $parent = null , bool $subject = false ) : array {
2017-05-18 13:21:17 -04:00
if ( is_null ( $id )) {
// if no ID is specified this is a no-op, unless a parent is specified, which is always a circular dependence
if ( ! is_null ( $parent )) {
throw new Db\ExceptionInput ( " circularDependence " , [ " action " => $this -> caller (), " field " => " parent " , 'id' => $parent ]);
}
return [ name => null , parent => null ];
}
// check whether the folder exists and is owned by the user
$f = $this -> db -> prepare ( " SELECT name,parent from arsse_folders where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> getRow ();
2017-05-21 10:10:36 -04:00
if ( ! $f ) throw new Db\ExceptionInput ( $subject ? " subjectMissing " : " idMissing " , [ " action " => $this -> caller (), " field " => " folder " , 'id' => $parent ]);
2017-05-18 13:21:17 -04:00
// if we're moving a folder to a new parent, check that the parent is valid
if ( ! is_null ( $parent )) {
// make sure both that the parent exists, and that the parent is not either the folder itself or one of its children (a circular dependence)
$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) " .
" SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ? " ,
" str " , " int " , " str " , " int "
) -> run ( $user , $id , $user , $parent ) -> getRow ();
if ( ! $p ) {
// if the parent doesn't exist or doesn't below to the user, throw an exception
throw new Db\ExceptionInput ( " idMissing " , [ " action " => $this -> caller (), " field " => " parent " , 'id' => $parent ]);
} else {
// if using the desired parent would create a circular dependence, throw a different exception
if ( ! $p [ 'valid' ]) throw new Db\ExceptionInput ( " circularDependence " , [ " action " => $this -> caller (), " field " => " parent " , 'id' => $parent ]);
}
}
return $f ;
}
protected function folderValidateName ( $name ) : bool {
$name = ( string ) $name ;
2017-05-21 10:10:36 -04:00
if ( ! strlen ( $name )) {
2017-05-18 13:21:17 -04:00
throw new Db\ExceptionInput ( " missing " , [ " action " => $this -> caller (), " field " => " name " ]);
} else if ( ! strlen ( trim ( $name ))) {
throw new Db\ExceptionInput ( " whitespace " , [ " action " => $this -> caller (), " field " => " name " ]);
} else {
return true ;
}
}
2017-03-31 18:48:24 -04:00
public function subscriptionAdd ( string $user , string $url , string $fetchUser = " " , string $fetchPassword = " " ) : int {
2017-04-13 22:17:53 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-05-04 11:13:24 -04:00
// check to see if the feed exists
2017-04-13 22:17:53 -04:00
$feedID = $this -> db -> prepare ( " SELECT id from arsse_feeds where url is ? and username is ? and password is ? " , " str " , " str " , " str " ) -> run ( $url , $fetchUser , $fetchPassword ) -> getValue ();
2017-05-04 14:42:40 -04:00
if ( is_null ( $feedID )) {
2017-05-04 11:13:24 -04:00
// if the feed doesn't exist add it to the database; we do this unconditionally so as to lock SQLite databases for as little time as possible
$feedID = $this -> db -> prepare ( 'INSERT INTO arsse_feeds(url,username,password) values(?,?,?)' , 'str' , 'str' , 'str' ) -> run ( $url , $fetchUser , $fetchPassword ) -> lastId ();
try {
// perform an initial update on the newly added feed
$this -> feedUpdate ( $feedID , true );
} catch ( \Throwable $e ) {
// if the update fails, delete the feed we just added
$this -> db -> prepare ( 'DELETE from arsse_feeds where id is ?' , 'int' ) -> run ( $feedID );
throw $e ;
}
2017-03-31 18:48:24 -04:00
}
2017-05-04 11:13:24 -04:00
// Add the feed to the user's subscriptions and return the new subscription's ID.
2017-04-13 22:17:53 -04:00
return $this -> db -> prepare ( 'INSERT INTO arsse_subscriptions(owner,feed) values(?,?)' , 'str' , 'int' ) -> run ( $user , $feedID ) -> lastId ();
}
2017-05-04 19:12:33 -04:00
public function subscriptionList ( string $user , int $folder = null , int $id = null ) : Db\Result {
2017-05-04 14:42:40 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-06-04 18:00:18 -04:00
// create a complex query
$q = new Query (
" SELECT
2017-05-04 19:12:33 -04:00
arsse_subscriptions . id ,
2017-05-18 13:21:17 -04:00
url , favicon , source , folder , pinned , err_count , err_msg , order_type ,
DATEFORMAT ( ? , added ) as added ,
2017-06-01 18:12:08 -04:00
topmost . top as top_folder ,
2017-06-10 13:29:46 -04:00
coalesce ( arsse_subscriptions . title , arsse_feeds . title ) as title ,
2017-06-04 18:00:18 -04:00
( SELECT count ( * ) from arsse_articles where feed is arsse_subscriptions . feed ) - ( SELECT count ( * ) from arsse_marks join user on user is owner join arsse_articles on article = arsse_articles . id where feed is arsse_feeds . id and read is 1 ) as unread
2017-06-10 13:29:46 -04:00
from arsse_subscriptions
join user on user is owner
join arsse_feeds on feed = arsse_feeds . id
left join topmost on folder = f_id " ,
2017-06-04 18:00:18 -04:00
" " , // where terms
" pinned desc, title " // order by terms
);
// define common table expressions
$q -> setCTE ( " user(user) as (SELECT ?) " , " str " , $user ); // the subject user; this way we only have to pass it to prepare() once
// topmost folders belonging to the user
$q -> setCTE ( " topmost(f_id,top) as (select id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id) " );
if ( ! is_null ( $id )) {
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings
$q -> setWhere ( " arsse_subscriptions.id is ? " , " int " , $id );
} else if ( ! is_null ( $folder )) {
// if a folder is specified, make sure it exists
$this -> folderValidateId ( $user , $folder );
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
$q -> setCTE ( " folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder) " , " int " , $folder );
// add a suitable WHERE condition
$q -> setWhere ( " folder in (select folder from folders) " );
}
return $this -> db -> prepare ( $q , " str " ) -> run ( $this -> dateFormatDefault );
2017-05-04 19:12:33 -04:00
}
public function subscriptionRemove ( string $user , int $id ) : bool {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-05-11 18:00:35 -04:00
$changes = $this -> db -> prepare ( " DELETE from arsse_subscriptions where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> changes ();
2017-05-21 10:10:36 -04:00
if ( ! $changes ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " folder " , 'id' => $id ]);
2017-05-11 18:00:35 -04:00
return true ;
2017-05-04 19:12:33 -04:00
}
public function subscriptionPropertiesGet ( string $user , int $id ) : array {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
// disable authorization checks for the list call
Data :: $user -> authorizationEnabled ( false );
$sub = $this -> subscriptionList ( $user , null , $id ) -> getRow ();
Data :: $user -> authorizationEnabled ( true );
2017-05-21 10:10:36 -04:00
if ( ! $sub ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " feed " , 'id' => $id ]);
2017-05-04 19:12:33 -04:00
return $sub ;
}
public function subscriptionPropertiesSet ( string $user , int $id , array $data ) : bool {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-05-06 12:02:27 -04:00
$tr = $this -> db -> begin ();
2017-05-04 19:12:33 -04:00
if ( ! $this -> db -> prepare ( " SELECT count(*) from arsse_subscriptions where owner is ? and id is ? " , " str " , " int " ) -> run ( $user , $id ) -> getValue ()) {
// if the ID doesn't exist or doesn't belong to the user, throw an exception
2017-05-21 10:10:36 -04:00
throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " feed " , 'id' => $id ]);
2017-05-04 14:42:40 -04:00
}
2017-05-18 13:21:17 -04:00
if ( array_key_exists ( " folder " , $data )) {
// ensure the target folder exists and belong to the user
$this -> folderValidateId ( $user , $data [ 'folder' ]);
}
if ( array_key_exists ( " title " , $data )) {
2017-05-21 10:10:36 -04:00
// if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string
if ( ! is_null ( $data [ 'title' ])) {
$title = ( string ) $data [ 'title' ];
if ( ! strlen ( $title )) throw new Db\ExceptionInput ( " missing " , [ " action " => __FUNCTION__ , " field " => " title " ]);
if ( ! strlen ( trim ( $title ))) throw new Db\ExceptionInput ( " whitespace " , [ " action " => __FUNCTION__ , " field " => " title " ]);
$data [ 'title' ] = $title ;
}
2017-05-18 13:21:17 -04:00
}
2017-05-04 19:12:33 -04:00
$valid = [
'title' => " str " ,
'folder' => " int " ,
'order_type' => " strict int " ,
'pinned' => " strict bool " ,
];
list ( $setClause , $setTypes , $setValues ) = $this -> generateSet ( $data , $valid );
2017-05-06 12:02:27 -04:00
$out = ( bool ) $this -> db -> prepare ( " UPDATE arsse_subscriptions set $setClause where owner is ? and id is ? " , $setTypes , " str " , " int " ) -> run ( $setValues , $user , $id ) -> changes ();
$tr -> commit ();
return $out ;
2017-05-04 14:42:40 -04:00
}
2017-05-18 23:03:33 -04:00
protected function subscriptionValidateId ( string $user , int $id ) : array {
$out = $this -> db -> prepare ( " SELECT feed from arsse_subscriptions where id is ? and owner is ? " , " int " , " str " ) -> run ( $id , $user ) -> getRow ();
if ( ! $out ) throw new Db\ExceptionInput ( " idMissing " , [ " action " => $this -> caller (), " field " => " subscription " , 'id' => $id ]);
return $out ;
}
public function feedListStale () : array {
2017-06-02 19:16:36 -04:00
$feeds = $this -> db -> prepare ( " SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP " ) -> run () -> getAll ();
return array_column ( $feeds , 'id' );
2017-05-18 23:03:33 -04:00
}
2017-04-30 18:36:31 -04:00
public function feedUpdate ( int $feedID , bool $throwError = false ) : bool {
2017-05-06 12:02:27 -04:00
$tr = $this -> db -> begin ();
// check to make sure the feed exists
2017-06-03 17:34:37 -04:00
$f = $this -> db -> prepare ( " SELECT url, username, password, DATEFORMAT('http', modified) AS lastmodified, etag, err_count FROM arsse_feeds where id is ? " , " int " ) -> run ( $feedID ) -> getRow ();
2017-05-21 10:10:36 -04:00
if ( ! $f ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => __FUNCTION__ , " field " => " feed " , 'id' => $feedID ]);
2017-05-06 12:02:27 -04:00
// the Feed object throws an exception when there are problems, but that isn't ideal
// here. When an exception is thrown it should update the database with the
// error instead of failing; if other exceptions are thrown, we should simply roll back
2017-04-09 18:15:00 -04:00
try {
2017-05-06 12:02:27 -04:00
$feed = new Feed ( $feedID , $f [ 'url' ], ( string ) $f [ 'lastmodified' ], $f [ 'etag' ], $f [ 'username' ], $f [ 'password' ]);
if ( ! $feed -> modified ) {
// if the feed hasn't changed, just compute the next fetch time and record it
2017-06-03 17:34:37 -04:00
$this -> db -> prepare ( " UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ? " , 'datetime' , 'int' ) -> run ( $feed -> nextFetch , $feedID );
2017-05-06 12:02:27 -04:00
$tr -> commit ();
2017-04-15 22:07:22 -04:00
return false ;
2017-04-20 21:59:12 -04:00
}
2017-05-06 12:02:27 -04:00
} catch ( Feed\Exception $e ) {
// update the database with the resultant error and the next fetch time, incrementing the error count
2017-04-30 17:54:29 -04:00
$this -> db -> prepare (
2017-06-03 14:08:33 -04:00
" UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ? " ,
2017-05-06 12:02:27 -04:00
'datetime' , 'str' , 'int'
) -> run ( Feed :: nextFetchOnError ( $f [ 'err_count' ]), $e -> getMessage (), $feedID );
$tr -> commit ();
if ( $throwError ) throw $e ;
return false ;
}
//prepare the necessary statements to perform the update
if ( sizeof ( $feed -> newItems ) || sizeof ( $feed -> changedItems )) {
2017-06-03 14:08:33 -04:00
$qInsertEnclosure = $this -> db -> prepare ( " INSERT INTO arsse_enclosures(article,url,type) values(?,?,?) " , 'int' , 'str' , 'str' );
$qInsertCategory = $this -> db -> prepare ( " INSERT INTO arsse_categories(article,name) values(?,?) " , 'int' , 'str' );
$qInsertEdition = $this -> db -> prepare ( " INSERT INTO arsse_editions(article) values(?) " , 'int' );
2017-05-06 12:02:27 -04:00
}
if ( sizeof ( $feed -> newItems )) {
$qInsertArticle = $this -> db -> prepare (
2017-06-03 17:34:37 -04:00
" INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?) " ,
2017-05-06 12:02:27 -04:00
'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'str' , 'str' , 'str' , 'str' , 'str' , 'int'
);
}
if ( sizeof ( $feed -> changedItems )) {
2017-06-03 17:34:37 -04:00
$qDeleteEnclosures = $this -> db -> prepare ( " DELETE FROM arsse_enclosures WHERE article is ? " , 'int' );
$qDeleteCategories = $this -> db -> prepare ( " DELETE FROM arsse_categories WHERE article is ? " , 'int' );
$qClearReadMarks = $this -> db -> prepare ( " UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article is ? and read is 1 " , 'int' );
2017-05-06 12:02:27 -04:00
$qUpdateArticle = $this -> db -> prepare (
2017-06-03 17:34:37 -04:00
" UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id is ? " ,
2017-05-06 12:02:27 -04:00
'str' , 'str' , 'str' , 'datetime' , 'datetime' , 'str' , 'str' , 'str' , 'str' , 'str' , 'int'
);
}
// actually perform updates
foreach ( $feed -> newItems as $article ) {
$articleID = $qInsertArticle -> run (
$article -> url ,
$article -> title ,
$article -> author ,
$article -> publishedDate ,
$article -> updatedDate ,
$article -> id ,
$article -> content ,
$article -> urlTitleHash ,
$article -> urlContentHash ,
$article -> titleContentHash ,
2017-04-13 22:17:53 -04:00
$feedID
2017-05-06 12:02:27 -04:00
) -> lastId ();
2017-06-03 14:08:33 -04:00
if ( $article -> enclosureUrl ) {
$qInsertEnclosure -> run ( $articleID , $article -> enclosureUrl , $article -> enclosureType );
}
foreach ( $article -> categories as $c ) {
2017-05-06 12:02:27 -04:00
$qInsertCategory -> run ( $articleID , $c );
}
$qInsertEdition -> run ( $articleID );
}
foreach ( $feed -> changedItems as $articleID => $article ) {
$qUpdateArticle -> run (
$article -> url ,
$article -> title ,
$article -> author ,
$article -> publishedDate ,
$article -> updatedDate ,
$article -> id ,
$article -> content ,
$article -> urlTitleHash ,
$article -> urlContentHash ,
$article -> titleContentHash ,
$articleID
2017-04-13 22:17:53 -04:00
);
2017-06-03 14:08:33 -04:00
$qDeleteEnclosures -> run ( $articleID );
2017-05-06 12:02:27 -04:00
$qDeleteCategories -> run ( $articleID );
2017-06-03 14:08:33 -04:00
if ( $article -> enclosureUrl ) {
$qInsertEnclosure -> run ( $articleID , $article -> enclosureUrl , $article -> enclosureType );
}
foreach ( $article -> categories as $c ) {
2017-05-06 12:02:27 -04:00
$qInsertCategory -> run ( $articleID , $c );
}
$qInsertEdition -> run ( $articleID );
$qClearReadMarks -> run ( $articleID );
2017-03-31 18:48:24 -04:00
}
2017-05-06 12:02:27 -04:00
// lastly update the feed database itself with updated information.
$this -> db -> prepare (
2017-06-03 17:34:37 -04:00
" UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ? WHERE id is ? " ,
2017-05-06 12:02:27 -04:00
'str' , 'str' , 'str' , 'str' , 'datetime' , 'str' , 'datetime' , 'int'
) -> run (
$feed -> data -> feedUrl ,
$feed -> data -> title ,
$feed -> favicon ,
$feed -> data -> siteUrl ,
$feed -> lastModified ,
$feed -> resource -> getEtag (),
$feed -> nextFetch ,
$feedID
);
$tr -> commit ();
2017-04-13 22:17:53 -04:00
return true ;
2017-03-31 18:48:24 -04:00
}
2017-04-15 22:07:22 -04:00
2017-05-30 20:18:04 -04:00
public function feedMatchLatest ( int $feedID , int $count ) : Db\Result {
2017-04-22 23:40:57 -04:00
return $this -> db -> prepare (
2017-06-03 17:34:37 -04:00
" SELECT id, DATEFORMAT('unix', edited) AS edited_date, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ? " ,
2017-04-22 23:40:57 -04:00
'int' , 'int'
) -> run ( $feedID , $count );
}
2017-05-30 20:18:04 -04:00
public function feedMatchIds ( int $feedID , array $ids = [], array $hashesUT = [], array $hashesUC = [], array $hashesTC = []) : Db\Result {
2017-04-22 23:40:57 -04:00
// compile SQL IN() clauses and necessary type bindings for the four identifier lists
2017-06-03 17:34:37 -04:00
list ( $cId , $tId ) = $this -> generateIn ( $ids , " str " );
2017-04-22 23:40:57 -04:00
list ( $cHashUT , $tHashUT ) = $this -> generateIn ( $hashesUT , " str " );
list ( $cHashUC , $tHashUC ) = $this -> generateIn ( $hashesUC , " str " );
list ( $cHashTC , $tHashTC ) = $this -> generateIn ( $hashesTC , " str " );
// perform the query
return $articles = $this -> db -> prepare (
2017-06-03 17:34:37 -04:00
" SELECT id, DATEFORMAT('unix', edited) AS edited_date, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? and (guid in( $cId ) or url_title_hash in( $cHashUT ) or url_content_hash in( $cHashUC ) or title_content_hash in( $cHashTC )) " ,
2017-04-22 23:40:57 -04:00
'int' , $tId , $tHashUT , $tHashUC , $tHashTC
) -> run ( $feedID , $ids , $hashesUT , $hashesUC , $hashesTC );
}
2017-06-04 08:15:10 -04:00
public function articleStarredCount ( string $user , array $context = []) : int {
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-07-05 10:59:13 -04:00
return $this -> db -> prepare (
" WITH RECURSIVE
user ( user ) as ( SELECT ? ),
subscribed_feeds ( id , sub ) as ( SELECT feed , id from arsse_subscriptions join user on user is owner ) " .
" SELECT count(*) from arsse_marks
join user on user is owner
join arsse_articles on arsse_marks . article is arsse_articles . id
join subscribed_feeds on arsse_articles . feed is subscribed_feeds . id
where starred is 1 " ,
" str "
) -> run ( $user ) -> getValue ();
2017-06-04 08:15:10 -04:00
}
2017-06-18 10:23:37 -04:00
public function editionLatest ( string $user , Context $context = null ) : int {
2017-06-04 08:15:10 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-06-18 10:23:37 -04:00
if ( ! $context ) $context = new Context ;
$q = new Query ( " SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article is arsse_articles.id left join arsse_feeds on arsse_articles.feed is arsse_feeds.id " );
if ( $context -> subscription ()) {
// if a subscription is specified, make sure it exists
$id = $this -> subscriptionValidateId ( $user , $context -> subscription )[ 'feed' ];
// a simple WHERE clause is required here
$q -> setWhere ( " arsse_feeds.id is ? " , " int " , $id );
} else {
$q -> setCTE ( " user(user) as (SELECT ?) " , " str " , $user );
2017-07-05 10:59:13 -04:00
$q -> setCTE (
" feeds(feed) as (SELECT feed from arsse_subscriptions join user on user is owner) " ,
[], // binding types
[], // binding values
" join feeds on arsse_articles.feed is feeds.feed " // join expression
);
2017-06-18 10:23:37 -04:00
}
return ( int ) $this -> db -> prepare ( $q ) -> run () -> getValue ();
}
public function articleList ( string $user , Context $context = null ) : Db\Result {
2017-06-04 18:00:18 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
2017-06-18 10:23:37 -04:00
if ( ! $context ) $context = new Context ;
$q = new Query (
2017-06-04 18:00:18 -04:00
" SELECT
2017-06-22 13:07:56 -04:00
arsse_articles . id as id ,
arsse_articles . url as url ,
title , author , content , guid ,
2017-07-05 09:09:38 -04:00
DATEFORMAT ( ? , published ) as published_date ,
DATEFORMAT ( ? , edited ) as edited_date ,
2017-06-10 13:29:46 -04:00
DATEFORMAT ( ? , max (
modified ,
2017-06-29 23:56:43 -04:00
coalesce (( select modified from arsse_marks join user on user is owner where article is arsse_articles . id ), '' )
2017-07-05 09:09:38 -04:00
)) as modified_date ,
2017-06-29 23:56:43 -04:00
NOT ( select count ( * ) from arsse_marks join user on user is owner where article is arsse_articles . id and read is 1 ) as unread ,
( select count ( * ) from arsse_marks join user on user is owner where article is arsse_articles . id and starred is 1 ) as starred ,
( select max ( id ) from arsse_editions where article is arsse_articles . id ) as edition ,
2017-06-22 13:07:56 -04:00
subscribed_feeds . sub as subscription ,
2017-06-04 18:00:18 -04:00
url_title_hash || ':' || url_content_hash || ':' || title_content_hash as fingerprint ,
arsse_enclosures . url as media_url ,
arsse_enclosures . type as media_type
FROM arsse_articles
join subscribed_feeds on arsse_articles . feed is subscribed_feeds . id
left join arsse_enclosures on arsse_enclosures . article is arsse_articles . id
" ,
2017-06-18 10:23:37 -04:00
" " , // WHERE clause
2017-06-29 23:56:43 -04:00
" edition " . ( $context -> reverse ? " desc " : " " ), // ORDER BY clause
2017-06-18 10:23:37 -04:00
$context -> limit ,
$context -> offset
);
$q -> setCTE ( " user(user) as (SELECT ?) " , " str " , $user );
if ( $context -> subscription ()) {
// if a subscription is specified, make sure it exists
$id = $this -> subscriptionValidateId ( $user , $context -> subscription )[ 'feed' ];
// add a basic CTE that will join in only the requested subscription
2017-06-22 13:07:56 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT ?,?) " , [ " int " , " int " ], [ $id , $context -> subscription ]);
2017-06-18 10:23:37 -04:00
} else if ( $context -> folder ()) {
// if a folder is specified, make sure it exists
$this -> folderValidateId ( $user , $context -> folder );
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
$q -> setCTE ( " folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder) " , " int " , $context -> folder );
// add another CTE for the subscriptions within the folder
2017-06-22 13:07:56 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder) " );
2017-06-18 10:23:37 -04:00
} else {
// otherwise add a CTE for all the user's subscriptions
2017-06-22 13:07:56 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner) " );
2017-06-18 10:23:37 -04:00
}
// filter based on edition offset
2017-06-22 13:07:56 -04:00
if ( $context -> oldestEdition ()) $q -> setWhere ( " edition >= ? " , " int " , $context -> oldestEdition );
if ( $context -> latestEdition ()) $q -> setWhere ( " edition <= ? " , " int " , $context -> latestEdition );
2017-06-18 10:23:37 -04:00
// filter based on lastmod time
2017-07-05 09:09:38 -04:00
if ( $context -> modifiedSince ()) $q -> setWhere ( " modified_date >= ? " , " datetime " , $context -> modifiedSince );
if ( $context -> notModifiedSince ()) $q -> setWhere ( " modified_date <= ? " , " datetime " , $context -> notModifiedSince );
2017-06-18 10:23:37 -04:00
// filter for un/read and un/starred status if specified
if ( $context -> unread ()) $q -> setWhere ( " unread is ? " , " bool " , $context -> unread );
if ( $context -> starred ()) $q -> setWhere ( " starred is ? " , " bool " , $context -> starred );
// perform the query and return results
2017-06-22 13:07:56 -04:00
return $this -> db -> prepare ( $q , " str " , " str " , " str " ) -> run ( $this -> dateFormatDefault , $this -> dateFormatDefault , $this -> dateFormatDefault );
2017-06-18 10:23:37 -04:00
}
2017-06-29 23:56:43 -04:00
public function articleMark ( string $user , array $data , Context $context = null ) : bool {
2017-06-18 10:23:37 -04:00
if ( ! Data :: $user -> authorize ( $user , __FUNCTION__ )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => __FUNCTION__ , " user " => $user ]);
if ( ! $context ) $context = new Context ;
// sanitize input
2017-06-29 23:56:43 -04:00
$values = [
isset ( $data [ 'read' ]) ? $data [ 'read' ] : null ,
isset ( $data [ 'starred' ]) ? $data [ 'starred' ] : null ,
2017-06-18 10:23:37 -04:00
];
// the two queries we want to execute to make the requested changes
$queries = [
2017-06-29 23:56:43 -04:00
" UPDATE arsse_marks
set
read = coalesce (( select read from target_values ), read ),
starred = coalesce (( select starred from target_values ), starred ),
modified = CURRENT_TIMESTAMP
WHERE
owner is ( select user from user )
and article in ( select id from target_articles where to_update is 1 ) " ,
" INSERT INTO arsse_marks(owner,article,read,starred)
select
( select user from user ),
id ,
coalesce (( select read from target_values ), 0 ),
coalesce (( select starred from target_values ), 0 )
from target_articles where to_insert is 1 "
2017-06-18 10:23:37 -04:00
];
$out = 0 ;
// wrap this UPDATE and INSERT together into a transaction
$tr = $this -> begin ();
2017-06-30 13:53:19 -04:00
// if an edition context is specified, make sure it's valid
if ( $context -> edition ()) {
// make sure the edition exists
$edition = $this -> articleValidateEdition ( $user , $context -> edition );
2017-07-05 09:09:38 -04:00
// if the edition is not the latest, do not mark the read flag
if ( ! $edition [ 'current' ]) $values [ 0 ] = null ;
2017-06-30 13:53:19 -04:00
} else if ( $context -> article ()) {
// otherwise if an article context is specified, make sure it's valid
$this -> articleValidateId ( $user , $context -> article );
}
2017-06-18 10:23:37 -04:00
// execute each query in sequence
foreach ( $queries as $query ) {
// first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles
$q = new Query (
" SELECT
arsse_articles . id as id ,
2017-06-29 23:56:43 -04:00
( select max ( id ) from arsse_editions where article is arsse_articles . id ) as edition ,
max ( arsse_articles . modified ,
coalesce (( select modified from arsse_marks join user on user is owner where article is arsse_articles . id ), '' )
2017-07-05 09:09:38 -04:00
) as modified_date ,
2017-06-29 23:56:43 -04:00
(
not exists ( select id from arsse_marks join user on user is owner where article is arsse_articles . id )
and exists ( select * from target_values where read is 1 or starred is 1 )
) as to_insert ,
exists (
select id from arsse_marks
join user on user is owner
where
article is arsse_articles . id
and (
read is not coalesce (( select read from target_values ), read )
or starred is not coalesce (( select starred from target_values ), starred )
)
) as to_update
2017-06-30 13:53:19 -04:00
FROM arsse_articles "
2017-06-18 10:23:37 -04:00
);
2017-06-29 23:56:43 -04:00
// common table expression for the affected user
2017-06-18 10:23:37 -04:00
$q -> setCTE ( " user(user) as (SELECT ?) " , " str " , $user );
2017-06-29 23:56:43 -04:00
// common table expression with the values to set
$q -> setCTE ( " target_values(read,starred) as (select ?,?) " , [ " bool " , " bool " ], $values );
2017-06-30 13:53:19 -04:00
if ( $context -> edition ()) {
2017-07-05 09:09:38 -04:00
// if an edition is specified, filter for its previously identified article
2017-06-30 13:53:19 -04:00
$q -> setWhere ( " arsse_articles.id is ? " , " int " , $edition [ 'article' ]);
} else if ( $context -> article ()) {
2017-07-05 09:09:38 -04:00
// if an article is specified, filter for it (it has already been validated above)
2017-06-30 13:53:19 -04:00
$q -> setWhere ( " arsse_articles.id is ? " , " int " , $context -> article );
} else if ( $context -> subscription ()) {
2017-06-18 10:23:37 -04:00
// if a subscription is specified, make sure it exists
$id = $this -> subscriptionValidateId ( $user , $context -> subscription )[ 'feed' ];
// add a basic CTE that will join in only the requested subscription
2017-06-30 13:53:19 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT ?,?) " , [ " int " , " int " ], [ $id , $context -> subscription ], " join subscribed_feeds on feed is subscribed_feeds.id " );
2017-06-18 10:23:37 -04:00
} else if ( $context -> folder ()) {
// if a folder is specified, make sure it exists
$this -> folderValidateId ( $user , $context -> folder );
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
$q -> setCTE ( " folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder) " , " int " , $context -> folder );
// add another CTE for the subscriptions within the folder
2017-06-30 13:53:19 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder) " , [], [], " join subscribed_feeds on feed is subscribed_feeds.id " );
2017-06-18 10:23:37 -04:00
} else {
// otherwise add a CTE for all the user's subscriptions
2017-06-30 13:53:19 -04:00
$q -> setCTE ( " subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner) " , [], [], " join subscribed_feeds on feed is subscribed_feeds.id " );
2017-06-18 10:23:37 -04:00
}
2017-06-22 13:07:56 -04:00
// filter based on edition offset
if ( $context -> oldestEdition ()) $q -> setWhere ( " edition >= ? " , " int " , $context -> oldestEdition );
if ( $context -> latestEdition ()) $q -> setWhere ( " edition <= ? " , " int " , $context -> latestEdition );
2017-06-18 10:23:37 -04:00
// filter based on lastmod time
2017-07-05 09:09:38 -04:00
if ( $context -> modifiedSince ()) $q -> setWhere ( " modified_date >= ? " , " datetime " , $context -> modifiedSince );
if ( $context -> notModifiedSince ()) $q -> setWhere ( " modified_date <= ? " , " datetime " , $context -> notModifiedSince );
2017-06-18 10:23:37 -04:00
// push the current query onto the CTE stack and execute the query we're actually interested in
$q -> pushCTE (
2017-06-29 23:56:43 -04:00
" target_articles(id,edition,modified,to_insert,to_update) " , // CTE table specification
2017-06-18 10:23:37 -04:00
[], // CTE types
[], // CTE values
2017-06-29 23:56:43 -04:00
$query // new query body
2017-06-18 10:23:37 -04:00
);
2017-06-29 23:56:43 -04:00
$out += $this -> db -> prepare ( $q ) -> run () -> changes ();
2017-06-18 10:23:37 -04:00
}
// commit the transaction
$tr -> commit ();
return ( bool ) $out ;
2017-06-04 18:00:18 -04:00
}
2017-06-30 13:53:19 -04:00
public function articleValidateId ( string $user , int $id ) : array {
$out = $this -> db -> prepare (
" SELECT
arsse_articles . id as article ,
( select max ( id ) from arsse_editions where article is arsse_articles . id ) as edition
FROM arsse_articles
join arsse_feeds on arsse_feeds . id is arsse_articles . feed
join arsse_subscriptions on arsse_subscriptions . feed is arsse_feeds . id
WHERE
arsse_articles . id is ? and arsse_subscriptions . owner is ? " ,
" int " , " str "
) -> run ( $id , $user ) -> getRow ();
2017-07-05 10:59:13 -04:00
if ( ! $out ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => $this -> caller (), " field " => " article " , 'id' => $id ]);
2017-06-30 13:53:19 -04:00
return $out ;
}
2017-07-05 09:09:38 -04:00
protected function articleValidateEdition ( string $user , int $id ) : array {
2017-06-30 13:53:19 -04:00
$out = $this -> db -> prepare (
" SELECT
arsse_editions . id as edition ,
arsse_editions . article as article ,
( arsse_editions . id is ( select max ( id ) from arsse_editions where article is arsse_editions . article )) as current
FROM arsse_editions
join arsse_articles on arsse_editions . article is arsse_articles . id
join arsse_feeds on arsse_feeds . id is arsse_articles . feed
join arsse_subscriptions on arsse_subscriptions . feed is arsse_feeds . id
WHERE
edition is ? and arsse_subscriptions . owner is ? " ,
" int " , " str "
) -> run ( $id , $user ) -> getRow ();
2017-07-05 10:59:13 -04:00
if ( ! $out ) throw new Db\ExceptionInput ( " subjectMissing " , [ " action " => $this -> caller (), " field " => " edition " , 'id' => $id ]);
2017-06-30 13:53:19 -04:00
return $out ;
}
2017-06-10 13:29:46 -04:00
}