2018-04-17 04:21:51 +02:00
< ? php
/**
* Name : Advanced content Filter
* Description : Expression - based content filter
* Version : 1.0
* Author : Hypolite Petovan < https :// friendica . mrpetovan . com / profile / hypolite >
* Maintainer : Hypolite Petovan < https :// friendica . mrpetovan . com / profile / hypolite >
*
* Copyright ( c ) 2018 Hypolite Petovan
* All rights reserved .
*
* Redistribution and use in source and binary forms , with or without
* modification , are permitted provided that the following conditions are met :
* * Redistributions of source code must retain the above copyright notice ,
* this list of conditions and the following disclaimer .
* * Redistributions in binary form must reproduce the above
* * copyright notice , this list of conditions and the following disclaimer in
* the documentation and / or other materials provided with the distribution .
* * Neither the name of Friendica nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission .
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS " AS IS " AND
* ANY EXPRESS OR IMPLIED WARRANTIES , INCLUDING , BUT NOT LIMITED TO , THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED . IN NO EVENT SHALL FRIENDICA BE LIABLE FOR ANY DIRECT ,
* INDIRECT , INCIDENTAL , SPECIAL , EXEMPLARY , OR CONSEQUENTIAL DAMAGES ( INCLUDING ,
* BUT NOT LIMITED TO , PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES ; LOSS OF USE ,
* DATA , OR PROFITS ; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY , WHETHER IN CONTRACT , STRICT LIABILITY , OR TORT ( INCLUDING NEGLIGENCE
* OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE , EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE .
*
*/
use Friendica\App ;
2018-10-17 21:34:15 +02:00
use Friendica\BaseModule ;
2018-07-20 04:18:02 +02:00
use Friendica\Content\Text\Markdown ;
2018-12-26 08:28:16 +01:00
use Friendica\Core\Hook ;
2018-10-30 00:40:18 +01:00
use Friendica\Core\Logger ;
2018-10-31 15:55:15 +01:00
use Friendica\Core\Renderer ;
2018-07-20 14:20:48 +02:00
use Friendica\Database\DBA ;
2018-04-17 04:21:51 +02:00
use Friendica\Database\DBStructure ;
2020-01-10 20:05:27 +01:00
use Friendica\DI ;
2018-07-20 04:18:02 +02:00
use Friendica\Model\Item ;
2021-01-17 01:00:32 +01:00
use Friendica\Model\Post ;
2020-05-06 00:47:43 +02:00
use Friendica\Model\Tag ;
2019-12-30 03:55:37 +01:00
use Friendica\Module\Security\Login ;
2018-04-17 04:21:51 +02:00
use Friendica\Network\HTTPException ;
2018-07-20 04:18:02 +02:00
use Friendica\Util\DateTimeFormat ;
2018-04-17 04:21:51 +02:00
use Psr\Http\Message\ResponseInterface ;
use Psr\Http\Message\ServerRequestInterface ;
use Symfony\Component\ExpressionLanguage ;
require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php' ;
2019-02-03 22:46:49 +01:00
function advancedcontentfilter_install ( App $a )
2018-04-17 04:21:51 +02:00
{
2019-12-23 01:36:32 +01:00
Hook :: register ( 'dbstructure_definition' , __FILE__ , 'advancedcontentfilter_dbstructure_definition' );
2018-12-26 08:28:16 +01:00
Hook :: register ( 'prepare_body_content_filter' , __FILE__ , 'advancedcontentfilter_prepare_body_content_filter' );
Hook :: register ( 'addon_settings' , __FILE__ , 'advancedcontentfilter_addon_settings' );
2018-04-17 04:21:51 +02:00
2019-12-23 01:36:32 +01:00
Hook :: add ( 'dbstructure_definition' , __FILE__ , 'advancedcontentfilter_dbstructure_definition' );
2021-01-30 14:32:43 +01:00
DBStructure :: performUpdate ();
2018-04-17 04:21:51 +02:00
2018-10-30 00:40:18 +01:00
Logger :: log ( " installed advancedcontentfilter " );
2018-04-17 04:21:51 +02:00
}
/*
* Hooks
*/
function advancedcontentfilter_dbstructure_definition ( App $a , & $database )
{
$database [ " advancedcontentfilter_rules " ] = [
" comment " => " Advancedcontentfilter addon rules " ,
" fields " => [
" id " => [ " type " => " int unsigned " , " not null " => " 1 " , " extra " => " auto_increment " , " primary " => " 1 " , " comment " => " Auto incremented rule id " ],
" uid " => [ " type " => " int unsigned " , " not null " => " 1 " , " comment " => " Owner user id " ],
" name " => [ " type " => " varchar(255) " , " not null " => " 1 " , " comment " => " Rule name " ],
" expression " => [ " type " => " mediumtext " , " not null " => " 1 " , " comment " => " Expression text " ],
" serialized " => [ " type " => " mediumtext " , " not null " => " 1 " , " comment " => " Serialized parsed expression " ],
" active " => [ " type " => " boolean " , " not null " => " 1 " , " default " => " 1 " , " comment " => " Whether the rule is active or not " ],
2018-10-21 08:53:07 +02:00
" created " => [ " type " => " datetime " , " not null " => " 1 " , " default " => DBA :: NULL_DATETIME , " comment " => " Creation date " ],
2018-04-17 04:21:51 +02:00
],
" indexes " => [
" PRIMARY " => [ " id " ],
" uid_active " => [ " uid " , " active " ],
]
];
}
function advancedcontentfilter_prepare_body_content_filter ( App $a , & $hook_data )
{
static $expressionLanguage ;
if ( is_null ( $expressionLanguage )) {
$expressionLanguage = new ExpressionLanguage\ExpressionLanguage ();
}
if ( ! local_user ()) {
return ;
}
$vars = [];
foreach ( $hook_data [ 'item' ] as $key => $value ) {
$vars [ str_replace ( '-' , '_' , $key )] = $value ;
}
2020-01-10 20:05:27 +01:00
$rules = DI :: cache () -> get ( 'rules_' . local_user ());
2018-04-17 04:21:51 +02:00
if ( ! isset ( $rules )) {
2018-07-21 04:16:16 +02:00
$rules = DBA :: toArray ( DBA :: select (
2018-04-17 04:21:51 +02:00
'advancedcontentfilter_rules' ,
[ 'name' , 'expression' , 'serialized' ],
[ 'uid' => local_user (), 'active' => true ]
));
2020-01-10 20:05:27 +01:00
DI :: cache () -> set ( 'rules_' . local_user (), $rules );
2018-04-17 04:21:51 +02:00
}
2018-05-01 14:39:15 +02:00
if ( $rules ) {
foreach ( $rules as $rule ) {
try {
$serializedParsedExpression = new ExpressionLanguage\SerializedParsedExpression (
$rule [ 'expression' ],
$rule [ 'serialized' ]
);
2018-08-23 02:27:00 +02:00
// The error suppression operator is used because of potentially broken user-supplied regular expressions
$found = ( bool ) @ $expressionLanguage -> evaluate ( $serializedParsedExpression , $vars );
2018-05-01 14:39:15 +02:00
} catch ( Exception $e ) {
$found = false ;
}
if ( $found ) {
2020-01-18 20:52:33 +01:00
$hook_data [ 'filter_reasons' ][] = DI :: l10n () -> t ( 'Filtered by rule: %s' , $rule [ 'name' ]);
2018-05-01 14:39:15 +02:00
break ;
}
2018-04-17 04:21:51 +02:00
}
}
}
function advancedcontentfilter_addon_settings ( App $a , & $s )
{
if ( ! local_user ()) {
return ;
}
2020-01-18 20:52:33 +01:00
$advancedcontentfilter = DI :: l10n () -> t ( 'Advanced Content Filter' );
2018-04-17 04:21:51 +02:00
$s .= <<< HTML
< span class = " settings-block fakelink " style = " display: block; " >< h3 >< a href = " advancedcontentfilter " > $advancedcontentfilter < i class = " glyphicon glyphicon-share " ></ i ></ a ></ h3 ></ span >
HTML ;
return ;
}
/*
* Module
*/
function advancedcontentfilter_module () {}
function advancedcontentfilter_init ( App $a )
{
2018-08-05 14:26:24 +02:00
if ( $a -> argc > 1 && $a -> argv [ 1 ] == 'api' ) {
2018-04-17 04:21:51 +02:00
$slim = new \Slim\App ();
require __DIR__ . '/src/middlewares.php' ;
require __DIR__ . '/src/routes.php' ;
$slim -> run ();
exit ;
}
}
function advancedcontentfilter_content ( App $a )
{
if ( ! local_user ()) {
2018-07-20 04:18:02 +02:00
return Login :: form ( '/' . implode ( '/' , $a -> argv ));
2018-04-17 04:21:51 +02:00
}
2018-08-05 14:26:24 +02:00
if ( $a -> argc > 1 && $a -> argv [ 1 ] == 'help' ) {
2018-04-17 04:21:51 +02:00
$lang = $a -> user [ 'language' ];
$default_dir = 'addon/advancedcontentfilter/doc/' ;
$help_file = 'advancedcontentfilter.md' ;
$help_path = $default_dir . $help_file ;
if ( file_exists ( $default_dir . $lang . '/' . $help_file )) {
$help_path = $default_dir . $lang . '/' . $help_file ;
}
$content = file_get_contents ( $help_path );
2018-07-20 04:18:02 +02:00
$html = Markdown :: convert ( $content , false );
2018-04-17 04:21:51 +02:00
$html = str_replace ( 'code>' , 'key>' , $html );
return $html ;
} else {
2018-10-31 15:55:15 +01:00
$t = Renderer :: getMarkupTemplate ( 'settings.tpl' , 'addon/advancedcontentfilter/' );
return Renderer :: replaceMacros ( $t , [
2018-08-05 14:26:48 +02:00
'$messages' => [
2020-01-18 20:52:33 +01:00
'backtosettings' => DI :: l10n () -> t ( 'Back to Addon Settings' ),
'title' => DI :: l10n () -> t ( 'Advanced Content Filter' ),
'add_a_rule' => DI :: l10n () -> t ( 'Add a Rule' ),
'help' => DI :: l10n () -> t ( 'Help' ),
'intro' => DI :: l10n () -> t ( 'Add and manage your personal content filter rules in this screen. Rules have a name and an arbitrary expression that will be matched against post data. For a complete reference of the available operations and variables, check the help page.' ),
'your_rules' => DI :: l10n () -> t ( 'Your rules' ),
'no_rules' => DI :: l10n () -> t ( 'You have no rules yet! Start adding one by clicking on the button above next to the title.' ),
'disabled' => DI :: l10n () -> t ( 'Disabled' ),
'enabled' => DI :: l10n () -> t ( 'Enabled' ),
'disable_this_rule' => DI :: l10n () -> t ( 'Disable this rule' ),
'enable_this_rule' => DI :: l10n () -> t ( 'Enable this rule' ),
'edit_this_rule' => DI :: l10n () -> t ( 'Edit this rule' ),
'edit_the_rule' => DI :: l10n () -> t ( 'Edit the rule' ),
'save_this_rule' => DI :: l10n () -> t ( 'Save this rule' ),
'delete_this_rule' => DI :: l10n () -> t ( 'Delete this rule' ),
'rule' => DI :: l10n () -> t ( 'Rule' ),
'close' => DI :: l10n () -> t ( 'Close' ),
'addtitle' => DI :: l10n () -> t ( 'Add new rule' ),
'rule_name' => DI :: l10n () -> t ( 'Rule Name' ),
'rule_expression' => DI :: l10n () -> t ( 'Rule Expression' ),
'cancel' => DI :: l10n () -> t ( 'Cancel' ),
2018-08-05 14:26:48 +02:00
],
2018-05-01 14:39:45 +02:00
'$current_theme' => $a -> getCurrentTheme (),
2018-04-17 04:21:51 +02:00
'$rules' => advancedcontentfilter_get_rules (),
2018-10-17 21:34:15 +02:00
'$form_security_token' => BaseModule :: getFormSecurityToken ()
2018-04-17 04:21:51 +02:00
]);
}
}
/*
* Common functions
*/
function advancedcontentfilter_build_fields ( $data )
{
$fields = [];
if ( ! empty ( $data [ 'name' ])) {
$fields [ 'name' ] = $data [ 'name' ];
}
if ( ! empty ( $data [ 'expression' ])) {
$allowed_keys = [
'author_id' , 'author_link' , 'author_name' , 'author_avatar' ,
'owner_id' , 'owner_link' , 'owner_name' , 'owner_avatar' ,
'contact_id' , 'uid' , 'id' , 'parent' , 'uri' ,
'thr_parent' , 'parent_uri' ,
'content_warning' ,
'commented' , 'created' , 'edited' , 'received' ,
'verb' , 'object_type' , 'postopts' , 'plink' , 'guid' , 'wall' , 'private' , 'starred' ,
'title' , 'body' ,
'file' , 'event_id' , 'location' , 'coord' , 'app' , 'attach' ,
'rendered_hash' , 'rendered_html' , 'object' ,
'allow_cid' , 'allow_gid' , 'deny_cid' , 'deny_gid' ,
'item_id' , 'item_network' , 'author_thumb' , 'owner_thumb' ,
'network' , 'url' , 'name' , 'writable' , 'self' ,
'cid' , 'alias' ,
'event_created' , 'event_edited' , 'event_start' , 'event_finish' , 'event_summary' ,
'event_desc' , 'event_location' , 'event_type' , 'event_nofinish' , 'event_adjust' , 'event_ignore' ,
'children' , 'pagedrop' , 'tags' , 'hashtags' , 'mentions' ,
];
$expressionLanguage = new ExpressionLanguage\ExpressionLanguage ();
$parsedExpression = $expressionLanguage -> parse ( $data [ 'expression' ], $allowed_keys );
$serialized = serialize ( $parsedExpression -> getNodes ());
$fields [ 'expression' ] = $data [ 'expression' ];
$fields [ 'serialized' ] = $serialized ;
}
if ( isset ( $data [ 'active' ])) {
$fields [ 'active' ] = intval ( $data [ 'active' ]);
} else {
$fields [ 'active' ] = 1 ;
}
return $fields ;
}
/*
* API
*/
function advancedcontentfilter_get_rules ()
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
2018-07-21 04:16:16 +02:00
$rules = DBA :: toArray ( DBA :: select ( 'advancedcontentfilter_rules' , [], [ 'uid' => local_user ()]));
2018-04-17 04:21:51 +02:00
return json_encode ( $rules );
}
function advancedcontentfilter_get_rules_id ( ServerRequestInterface $request , ResponseInterface $response , $args )
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
$rule = DBA :: selectFirst ( 'advancedcontentfilter_rules' , [], [ 'id' => $args [ 'id' ], 'uid' => local_user ()]);
2018-04-17 04:21:51 +02:00
return json_encode ( $rule );
}
function advancedcontentfilter_post_rules ( ServerRequestInterface $request )
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
2018-10-17 21:34:15 +02:00
if ( ! BaseModule :: checkFormSecurityToken ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\BadRequestException ( DI :: l10n () -> t ( 'Invalid form security token, please refresh the page.' ));
2018-04-17 04:21:51 +02:00
}
$data = json_decode ( $request -> getBody (), true );
try {
$fields = advancedcontentfilter_build_fields ( $data );
} catch ( Exception $e ) {
2020-03-10 23:44:27 +01:00
throw new HTTPException\BadRequestException ( $e -> getMessage (), $e );
2018-04-17 04:21:51 +02:00
}
if ( empty ( $fields [ 'name' ]) || empty ( $fields [ 'expression' ])) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\BadRequestException ( DI :: l10n () -> t ( 'The rule name and expression are required.' ));
2018-04-17 04:21:51 +02:00
}
$fields [ 'uid' ] = local_user ();
2018-07-20 04:18:02 +02:00
$fields [ 'created' ] = DateTimeFormat :: utcNow ();
2018-04-17 04:21:51 +02:00
2018-07-20 14:20:48 +02:00
if ( ! DBA :: insert ( 'advancedcontentfilter_rules' , $fields )) {
2019-11-24 20:20:57 +01:00
throw new HTTPException\ServiceUnavailableException ( DBA :: errorMessage ());
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
$rule = DBA :: selectFirst ( 'advancedcontentfilter_rules' , [], [ 'id' => DBA :: lastInsertId ()]);
2018-04-17 04:21:51 +02:00
2020-01-18 20:52:33 +01:00
return json_encode ([ 'message' => DI :: l10n () -> t ( 'Rule successfully added' ), 'rule' => $rule ]);
2018-04-17 04:21:51 +02:00
}
function advancedcontentfilter_put_rules_id ( ServerRequestInterface $request , ResponseInterface $response , $args )
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
2018-10-17 21:34:15 +02:00
if ( ! BaseModule :: checkFormSecurityToken ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\BadRequestException ( DI :: l10n () -> t ( 'Invalid form security token, please refresh the page.' ));
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
if ( ! DBA :: exists ( 'advancedcontentfilter_rules' , [ 'id' => $args [ 'id' ], 'uid' => local_user ()])) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\NotFoundException ( DI :: l10n () -> t ( 'Rule doesn\'t exist or doesn\'t belong to you.' ));
2018-04-17 04:21:51 +02:00
}
$data = json_decode ( $request -> getBody (), true );
try {
$fields = advancedcontentfilter_build_fields ( $data );
} catch ( Exception $e ) {
2020-03-10 23:44:27 +01:00
throw new HTTPException\BadRequestException ( $e -> getMessage (), $e );
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
if ( ! DBA :: update ( 'advancedcontentfilter_rules' , $fields , [ 'id' => $args [ 'id' ]])) {
2020-01-19 16:29:54 +01:00
throw new HTTPException\ServiceUnavailableException ( DBA :: errorMessage ());
2018-04-17 04:21:51 +02:00
}
2020-01-18 20:52:33 +01:00
return json_encode ([ 'message' => DI :: l10n () -> t ( 'Rule successfully updated' )]);
2018-04-17 04:21:51 +02:00
}
function advancedcontentfilter_delete_rules_id ( ServerRequestInterface $request , ResponseInterface $response , $args )
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
2018-10-17 21:34:15 +02:00
if ( ! BaseModule :: checkFormSecurityToken ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\BadRequestException ( DI :: l10n () -> t ( 'Invalid form security token, please refresh the page.' ));
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
if ( ! DBA :: exists ( 'advancedcontentfilter_rules' , [ 'id' => $args [ 'id' ], 'uid' => local_user ()])) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\NotFoundException ( DI :: l10n () -> t ( 'Rule doesn\'t exist or doesn\'t belong to you.' ));
2018-04-17 04:21:51 +02:00
}
2018-07-20 14:20:48 +02:00
if ( ! DBA :: delete ( 'advancedcontentfilter_rules' , [ 'id' => $args [ 'id' ]])) {
2020-01-19 16:29:54 +01:00
throw new HTTPException\ServiceUnavailableException ( DBA :: errorMessage ());
2018-04-17 04:21:51 +02:00
}
2020-01-18 20:52:33 +01:00
return json_encode ([ 'message' => DI :: l10n () -> t ( 'Rule successfully deleted' )]);
2018-04-17 04:21:51 +02:00
}
function advancedcontentfilter_get_variables_guid ( ServerRequestInterface $request , ResponseInterface $response , $args )
{
if ( ! local_user ()) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\UnauthorizedException ( DI :: l10n () -> t ( 'You must be logged in to use this method' ));
2018-04-17 04:21:51 +02:00
}
if ( ! isset ( $args [ 'guid' ])) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\BadRequestException ( DI :: l10n () -> t ( 'Missing argument: guid.' ));
2018-04-17 04:21:51 +02:00
}
2018-06-16 00:31:16 +02:00
$condition = [ " `guid` = ? AND (`uid` = ? OR `uid` = 0) " , $args [ 'guid' ], local_user ()];
$params = [ 'order' => [ 'uid' => true ]];
2021-01-17 01:00:32 +01:00
$item = Post :: selectFirstForUser ( local_user (), [], $condition , $params );
2018-04-17 04:21:51 +02:00
2018-07-21 14:46:13 +02:00
if ( ! DBA :: isResult ( $item )) {
2020-01-18 20:52:33 +01:00
throw new HTTPException\NotFoundException ( DI :: l10n () -> t ( 'Unknown post with guid: %s' , $args [ 'guid' ]));
2018-04-17 04:21:51 +02:00
}
2020-05-23 16:30:55 +02:00
$tags = Tag :: populateFromItem ( $item );
2018-04-17 04:21:51 +02:00
$item [ 'tags' ] = $tags [ 'tags' ];
$item [ 'hashtags' ] = $tags [ 'hashtags' ];
$item [ 'mentions' ] = $tags [ 'mentions' ];
$return = [];
foreach ( $item as $key => $value ) {
$return [ str_replace ( '-' , '_' , $key )] = $value ;
}
2018-04-18 02:37:23 +02:00
return json_encode ([ 'variables' => str_replace ( '\\\'' , '\'' , var_export ( $return , true ))]);
2018-06-16 00:31:16 +02:00
}