From b02724f867b1f210d464c8e44d4a3f0d452b9d7f Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Mon, 16 Apr 2018 22:21:51 -0400 Subject: [PATCH] [advancedcontentfilter] Add addon files - Add hooks - Add mini-API module powered by Slim - Add addon settings page powered by VueJS - Add translation strings - Add help page --- advancedcontentfilter/LICENSE | 24 + advancedcontentfilter/README.md | 11 + .../advancedcontentfilter.js | 113 ++++ .../advancedcontentfilter.php | 420 ++++++++++++++ .../doc/advancedcontentfilter.md | 514 ++++++++++++++++++ advancedcontentfilter/lang/C/messages.po | 163 ++++++ advancedcontentfilter/src/middlewares.php | 30 + advancedcontentfilter/src/routes.php | 22 + advancedcontentfilter/templates/settings.tpl | 99 ++++ 9 files changed, 1396 insertions(+) create mode 100644 advancedcontentfilter/LICENSE create mode 100644 advancedcontentfilter/README.md create mode 100644 advancedcontentfilter/advancedcontentfilter.js create mode 100644 advancedcontentfilter/advancedcontentfilter.php create mode 100644 advancedcontentfilter/doc/advancedcontentfilter.md create mode 100644 advancedcontentfilter/lang/C/messages.po create mode 100644 advancedcontentfilter/src/middlewares.php create mode 100644 advancedcontentfilter/src/routes.php create mode 100644 advancedcontentfilter/templates/settings.tpl diff --git a/advancedcontentfilter/LICENSE b/advancedcontentfilter/LICENSE new file mode 100644 index 000000000..e877cc1ca --- /dev/null +++ b/advancedcontentfilter/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2011-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. diff --git a/advancedcontentfilter/README.md b/advancedcontentfilter/README.md new file mode 100644 index 000000000..de16d6f2c --- /dev/null +++ b/advancedcontentfilter/README.md @@ -0,0 +1,11 @@ +Advanced Content Filter +======================= + +Main author Hypolite Petovan. + + +## License + +The _Advanced Content Filter_ addon is licensed under the [3-clause BSD license][2] see the LICENSE file in the addons directory. + +[1]: http://opensource.org/licenses/BSD-3-Clause diff --git a/advancedcontentfilter/advancedcontentfilter.js b/advancedcontentfilter/advancedcontentfilter.js new file mode 100644 index 000000000..531aec746 --- /dev/null +++ b/advancedcontentfilter/advancedcontentfilter.js @@ -0,0 +1,113 @@ +Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('#csrf').getAttribute('value'); + +new Vue({ + el: '#rules', + + data: { + showModal: false, + errorMessage: '', + editedIndex: null, + rule: {id: '', name: '', expression: '', created: ''}, + rules: existingRules || [], + itemUrl: '', + itemJson: '' + }, + + watch: { + showModal: function () { + if (this.showModal) { + $(this.$refs.vuemodal).modal('show'); + } else { + $(this.$refs.vuemodal).modal('hide'); + } + } + }, + + //created: function () { + // this.fetchRules(); + //}, + + methods: { + resetForm: function() { + this.rule = {id: '', name: '', expression: '', created: ''}; + this.showModal = false; + this.editedIndex = null; + }, + + //fetchRules: function () { + // this.$http.get('/advancedcontentfilter/api/rules') + // .then(function (response) { + // this.rules = response.body; + // }, function (err) { + // console.log(err); + // }); + //}, + + addRule: function () { + if (this.rule.name.trim()) { + this.errorMessage = ''; + this.$http.post('/advancedcontentfilter/api/rules', this.rule) + .then(function (res) { + this.rules.push(res.body.rule); + this.resetForm(); + }, function (err) { + this.errorMessage = err.body.message; + }); + } + }, + + editRule: function (rule) { + this.editedIndex = this.rules.indexOf(rule); + this.rule = Object.assign({}, rule); + this.showModal = true; + }, + + saveRule: function (rule) { + this.errorMessage = ''; + this.$http.put('/advancedcontentfilter/api/rules/' + rule.id, rule) + .then(function (res) { + this.rules[this.editedIndex] = rule; + this.resetForm(); + }, function (err) { + this.errorMessage = err.body.message; + }); + }, + + toggleActive: function (rule) { + this.$http.put('/advancedcontentfilter/api/rules/' + rule.id, {'active': Math.abs(parseInt(rule.active) - 1)}) + .then(function (res) { + this.rules[this.rules.indexOf(rule)].active = Math.abs(parseInt(rule.active) - 1); + }, function (err) { + console.log(err); + }); + }, + + deleteRule: function (rule) { + if (confirm('Are you sure you want to delete this rule?')) { + this.$http.delete('/advancedcontentfilter/api/rules/' + rule.id) + .then(function (res) { + this.rules.splice(this.rules.indexOf(rule), 1); + }, function (err) { + console.log(err); + }); + } + }, + + showVariables: function () { + var guid = ''; + + var urlParts = this.itemUrl.split('/'); + + guid = urlParts[urlParts.length - 1]; + + this.$http.get('/advancedcontentfilter/api/variables/' + guid) + .then(function (response) { + this.itemJson = response.bodyText; + }, function (err) { + console.log(err); + }); + + return false; + } + } +}); \ No newline at end of file diff --git a/advancedcontentfilter/advancedcontentfilter.php b/advancedcontentfilter/advancedcontentfilter.php new file mode 100644 index 000000000..bc95cfa31 --- /dev/null +++ b/advancedcontentfilter/advancedcontentfilter.php @@ -0,0 +1,420 @@ + + * Maintainer: Hypolite Petovan + * + * 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; +use Friendica\Core\Addon; +use Friendica\Core\L10n; +use Friendica\Core\System; +use Friendica\Database\DBStructure; +use Friendica\Network\HTTPException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\ExpressionLanguage; + +require_once 'boot.php'; +require_once 'include/conversation.php'; +require_once 'include/dba.php'; +require_once 'include/security.php'; + +require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; + +function advancedcontentfilter_install() +{ + Addon::registerHook('dbstructure_definition' , __FILE__, 'advancedcontentfilter_dbstructure_definition'); + Addon::registerHook('prepare_body_content_filter', __FILE__, 'advancedcontentfilter_prepare_body_content_filter'); + Addon::registerHook('addon_settings' , __FILE__, 'advancedcontentfilter_addon_settings'); + + DBStructure::update(false, true); + + logger("installed advancedcontentfilter"); +} + +function advancedcontentfilter_uninstall() +{ + Addon::unregisterHook('dbstructure_definition' , __FILE__, 'advancedcontentfilter_dbstructure_definition'); + Addon::unregisterHook('prepare_body_content_filter', __FILE__, 'advancedcontentfilter_prepare_body_content_filter'); + Addon::unregisterHook('addon_settings' , __FILE__, 'advancedcontentfilter_addon_settings'); +} + +/* + * 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"], + "created" => ["type" => "datetime" , "not null" => "1", "default" => NULL_DATE, "comment" => "Creation date"], + ], + "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; + } + + $rules = Friendica\Core\Cache::get('rules_' . local_user()); + if (!isset($rules)) { + $rules = dba::inArray(dba::select( + 'advancedcontentfilter_rules', + ['name', 'expression', 'serialized'], + ['uid' => local_user(), 'active' => true] + )); + } + + foreach($rules as $rule) { + try { + $serializedParsedExpression = new ExpressionLanguage\SerializedParsedExpression( + $rule['expression'], + $rule['serialized'] + ); + + $found = (bool) $expressionLanguage->evaluate($serializedParsedExpression, $vars); + } catch (Exception $e) { + $found = false; + } + + if ($found) { + $hook_data['filter_reasons'][] = L10n::t('Filtered by rule: %s', $rule['name']); + break; + } + } +} + + +function advancedcontentfilter_addon_settings(App $a, &$s) +{ + if (!local_user()) { + return; + } + + $advancedcontentfilter = L10n::t('Advanced Content Filter'); + + $s .= <<

$advancedcontentfilter

+HTML; + + return; +} + +/* + * Module + */ + +function advancedcontentfilter_module() {} + +function advancedcontentfilter_init(App $a) +{ + if ($a->argv[1] == 'api') { + $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()) { + return \Friendica\Module\Login::form('/' . implode('/', $a->argv)); + } + + if ($a->argc > 0 && $a->argv[1] == 'help') { + $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); + + $html = \Friendica\Content\Text\Markdown::convert($content, false); + + $html = str_replace('code>', 'key>', $html); + + return $html; + } else { + $t = get_markup_template('settings.tpl', 'addon/advancedcontentfilter/'); + return replace_macros($t, [ + '$backtosettings' => L10n::t('Back to Addon Settings'), + '$title' => L10n::t('Advanced Content Filter'), + '$add_a_rule' => L10n::t('Add a Rule'), + '$help' => L10n::t('Help'), + '$advanced_content_filter_intro' => 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' => L10n::t('Your rules'), + '$no_rules' => L10n::t('You have no rules yet! Start adding one by clicking on the button above next to the title.'), + '$disabled' => L10n::t('Disabled'), + '$enabled' => L10n::t('Enabled'), + '$disable_this_rule' => L10n::t('Disable this rule'), + '$enable_this_rule' => L10n::t('Enable this rule'), + '$edit_this_rule' => L10n::t('Edit this rule'), + '$edit_the_rule' => L10n::t('Edit the rule'), + '$save_this_rule' => L10n::t('Save this rule'), + '$delete_this_rule' => L10n::t('Delete this rule'), + '$rule' => L10n::t('Rule'), + '$close' => L10n::t('Close'), + '$addtitle' => L10n::t('Add new rule'), + '$rule_name' => L10n::t('Rule Name'), + '$rule_expression' => L10n::t('Rule Expression'), + '$examples' => L10n::t('

Examples:

'), + '$cancel' => L10n::t('Cancel'), + '$rules' => advancedcontentfilter_get_rules(), + '$baseurl' => System::baseUrl(true), + '$form_security_token' => get_form_security_token() + ]); + } +} + +/* + * 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()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + $rules = dba::inArray(dba::select('advancedcontentfilter_rules', [], ['uid' => local_user()])); + + return json_encode($rules); +} + +function advancedcontentfilter_get_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!local_user()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + $rule = dba::selectFirst('advancedcontentfilter_rules', [], ['id' => $args['id'], 'uid' => local_user()]); + + return json_encode($rule); +} + +function advancedcontentfilter_post_rules(ServerRequestInterface $request) +{ + if (!local_user()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + if (!check_form_security_token()) { + throw new HTTPException\BadRequestException(L10n::t('Invalid form security token, please refresh the page.')); + } + + $data = json_decode($request->getBody(), true); + + try { + $fields = advancedcontentfilter_build_fields($data); + } catch (Exception $e) { + throw new HTTPException\BadRequestException($e->getMessage(), 0, $e); + } + + if (empty($fields['name']) || empty($fields['expression'])) { + throw new HTTPException\BadRequestException(L10n::t('The rule name and expression are required.')); + } + + $fields['uid'] = local_user(); + $fields['created'] = \Friendica\Util\DateTimeFormat::utcNow(); + + if (!dba::insert('advancedcontentfilter_rules', $fields)) { + throw new HTTPException\ServiceUnavaiableException(dba::errorMessage()); + } + + $rule = dba::selectFirst('advancedcontentfilter_rules', [], ['id' => dba::lastInsertId()]); + + return json_encode(['message' => L10n::t('Rule successfully added'), 'rule' => $rule]); +} + +function advancedcontentfilter_put_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!local_user()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + if (!check_form_security_token()) { + throw new HTTPException\BadRequestException(L10n::t('Invalid form security token, please refresh the page.')); + } + + if (!dba::exists('advancedcontentfilter_rules', ['id' => $args['id'], 'uid' => local_user()])) { + throw new HTTPException\NotFoundException(L10n::t('Rule doesn\'t exist or doesn\'t belong to you.')); + } + + $data = json_decode($request->getBody(), true); + + try { + $fields = advancedcontentfilter_build_fields($data); + } catch (Exception $e) { + throw new HTTPException\BadRequestException($e->getMessage(), 0, $e); + } + + if (!dba::update('advancedcontentfilter_rules', $fields, ['id' => $args['id']])) { + throw new HTTPException\ServiceUnavaiableException(dba::errorMessage()); + } + + return json_encode(['message' => L10n::t('Rule successfully updated')]); +} + +function advancedcontentfilter_delete_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!local_user()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + if (!check_form_security_token()) { + throw new HTTPException\BadRequestException(L10n::t('Invalid form security token, please refresh the page.')); + } + + if (!dba::exists('advancedcontentfilter_rules', ['id' => $args['id'], 'uid' => local_user()])) { + throw new HTTPException\NotFoundException(L10n::t('Rule doesn\'t exist or doesn\'t belong to you.')); + } + + if (!dba::delete('advancedcontentfilter_rules', ['id' => $args['id']])) { + throw new HTTPException\ServiceUnavaiableException(dba::errorMessage()); + } + + return json_encode(['message' => L10n::t('Rule successfully deleted')]); +} + +function advancedcontentfilter_get_variables_guid(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!local_user()) { + throw new HTTPException\UnauthorizedException(L10n::t('You must be logged in to use this method')); + } + + if (!isset($args['guid'])) { + throw new HTTPException\BadRequestException(L10n::t('Missing argument: guid.')); + } + + $item = dba::fetch_first(item_query() . " AND `item`.`guid` = ? AND (`item`.`uid` = ? OR `item`.`uid` = 0) ORDER BY `item`.`uid` DESC", $args['guid'], local_user()); + + if (!\Friendica\Database\DBM::is_result($item)) { + throw new HTTPException\NotFoundException(L10n::t('Unknown post with guid: %s', $args['guid'])); + } + + $tags = \Friendica\Model\Term::populateTagsFromItem($item); + + $item['tags'] = $tags['tags']; + $item['hashtags'] = $tags['hashtags']; + $item['mentions'] = $tags['mentions']; + + $return = []; + foreach ($item as $key => $value) { + $return[str_replace('-', '_', $key)] = $value; + } + + return str_replace('\\\'', '\'', var_export($return, true)); +} \ No newline at end of file diff --git a/advancedcontentfilter/doc/advancedcontentfilter.md b/advancedcontentfilter/doc/advancedcontentfilter.md new file mode 100644 index 000000000..4fcaf68aa --- /dev/null +++ b/advancedcontentfilter/doc/advancedcontentfilter.md @@ -0,0 +1,514 @@ + + +🔙 Back to Addon Settings + +# Advanced Content Filter Help + +The advanced Content Filter uses Symfony's Expression Language. +This help page includes a summary of [the Symfony's Expression Language documentation page.](https://symfony.com/doc/current/components/expression_language/syntax.html) + +## Basics + +The advanced content filter matches each post that is about to be displayed against each enabled rule you set. + +A rule is a boolean expression that should return either `true` or `false` depending on post variables. + +If the expression using a post variables returns `true`, the post will be collapsed and the matching rule name will be displayed above the collapsed content. + +A post will be collapsed if at least one rule matches, but all matching rule names will be displayed above the collapsed content. + +## Expression Syntax + +### Supported Literals + +- **strings** - single and double quotes (e.g. `'hello'`). +- **numbers** - e.g. `103`. +- **arrays** - using JSON-like notation (e.g. `[1, 2]`). +- **hashes** - using JSON-like notation (e.g. `{ foo: 'bar' }`). +- **booleans** - `true` and `false`. +- **null** - `null`. + +A backslash (``\``) must be escaped by 4 backslashes (``\\\\``) in a string +and 8 backslashes (``\\\\\\\\``) in a regex:: + +`"a\\\\b" matches "/^a\\\\\\\\b$/"` + +Control characters (e.g. ``\n``) in expressions are replaced with +whitespace. To avoid this, escape the sequence with a single backslash +(e.g. ``\\n``). + +### Supported Operators + +The component comes with a lot of operators: + +#### Arithmetic Operators + +* ``+`` (addition) +* ``-`` (subtraction) +* ``*`` (multiplication) +* ``/`` (division) +* ``%`` (modulus) +* ``**`` (pow) + +#### Bitwise Operators + +* ``&`` (and) +* ``|`` (or) +* ``^`` (xor) + +#### Comparison Operators + +* ``==`` (equal) +* ``===`` (identical) +* ``!=`` (not equal) +* ``!==`` (not identical) +* ``<`` (less than) +* ``>`` (greater than) +* ``<=`` (less than or equal to) +* ``>=`` (greater than or equal to) +* ``matches`` (regex match) + + To test if a string does *not* match a regex, use the logical ``not`` + operator in combination with the ``matches`` operator: + + 'not ("foo" matches "/bar/")' + + You must use parenthesis because the unary operator ``not`` has precedence + over the binary operator ``matches``. + +#### Logical Operators + +* ``not`` or ``!`` +* ``and`` or ``&&`` +* ``or`` or ``||`` + +#### String Operators + +* ``~`` (concatenation) + +For example: ``firstName ~ " " ~ lastName`` + +#### Array Operators + +* ``in`` (contain) +* ``not in`` (does not contain) + +For example: ``user.group in ["human_resources", "marketing"]`` + +#### Numeric Operators + +* ``..`` (range) + +For example: ``user.age in 18..45`` + +#### Ternary Operators + +* ``foo ? 'yes' : 'no'`` +* ``foo ?: 'no'`` (equal to ``foo ? foo : 'no'``) +* ``foo ? 'yes'`` (equal to ``foo ? 'yes' : ''``) + +### Supported variables + +Here are a sample of the available variables you can use in your expressions. +You can also retrieve the variables of a specific post by pasting its URL below the rule list. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeSample Value
author-idnumber6
author-linkstringhttps://friendica.mrpetovan.com/profile/hypolite
author-namestringHypolite Petovan
author-avatarstringhttps://friendica.mrpetovan.com/photo/41084997915a94a8c83cc39708500207-5.png
owner-idnumber6
owner-linkstringhttps://friendica.mrpetovan.com/profile/hypolite
owner-namestringHypolite Petovan
owner-avatarstringhttps://friendica.mrpetovan.com/photo/41084997915a94a8c83cc39708500207-5.png
contact-idnumber1
uidnumber1
idnumber791875
parentnumber791875
uristringurn:X-dfrn:friendica.mrpetovan.com:1:twit:978740198937907200
thr-parentstringurn:X-dfrn:friendica.mrpetovan.com:1:twit:978740198937907200
parent-uristringurn:X-dfrn:friendica.mrpetovan.com:1:twit:978740198937907200
content-warningstring
commenteddate2018-03-27 21:10:18
createddate2018-03-27 21:10:18
editeddate2018-03-27 21:10:18
receiveddate2018-03-27 21:10:18
verbstringhttp://activitystrea.ms/schema/1.0/post
object-typestringhttp://activitystrea.ms/schema/1.0/bookmark
postoptsstringtwitter&lang=pidgin;0.24032407407407:english;0.225:french;0.18055555555556
plinkstringhttps://friendica.mrpetovan.com/display/735a2029995abab33a5c006052376776
guidstring735a2029995abab33a5c006052376776
wallboolean1
privateboolean0
starredboolean0
titlestring
bodystringOver-compensation #[url=https://friendica.mrpetovan.com/search?tag=Street]Street[/url] #[url=https://friendica.mrpetovan.com/search?tag=Night]Night[/url] #[url=https://friendica.mrpetovan.com/search?tag=CarLights]CarLights[/url] #[url=https://friendica.mrpetovan.com/search?tag=Jeep]Jeep[/url] #[url=https://friendica.mrpetovan.com/search?tag=NoPeople]NoPeople[/url] #[url=https://friendica.mrpetovan.com/search?tag=Close]Close[/url]-up + [attachment type='link' url='https://www.eyeem.com/p/120800309' title='Over-compensation Street Night Car Lights Jeep No | EyeEm' image='https://cdn.eyeem.com/thumb/b2f019738cbeef06e2f8c9517c6286a8adcd3a00-1522184820641/640/480']Photo by @[url=https://twitter.com/MrPetovan]MrPetovan[/url][/attachment]
filestring
event-idnumbernull +
locationstring
coordstring
appstringEyeEm
attachstring
rendered-hashstringb70abdea8b362dc5dcf63e1b2836ad89
rendered-htmlstring + Over-compensation #<a href="https://friendica.mrpetovan.com/search?tag=Street" class="tag" title="Street">Street</a> #<a href="https://friendica.mrpetovan.com/search?tag=Night" class="tag" title="Night">Night</a> #<a href="https://friendica.mrpetovan.com/search?tag=CarLights" class="tag" title="CarLights">CarLights</a> #<a href="https://friendica.mrpetovan.com/search?tag=Jeep" class="tag" title="Jeep">Jeep</a> #<a href="https://friendica.mrpetovan.com/search?tag=NoPeople" class="tag" title="NoPeople">NoPeople</a> #<a href="https://friendica.mrpetovan.com/search?tag=Close" class="tag" title="Close">Close</a>-up <div class="type-link"><a href="https://www.eyeem.com/p/120800309" target="_blank"><img src="https://friendica.mrpetovan.com/proxy/bb/aHR0cHM6Ly9jZG4uZXllZW0uY29tL3RodW1iL2IyZjAxOTczOGNiZWVmMDZlMmY4Yzk1MTdjNjI4NmE4YWRjZDNhMDAtMTUyMjE4NDgyMDY0MS82NDAvNDgw" alt="" title="Over-compensation Street Night Car Lights Jeep No | EyeEm" class="attachment-image"></a><br><h4><a href="https://www.eyeem.com/p/120800309">Over-compensation Street Night Car Lights Jeep No | EyeEm</a></h4><blockquote>Photo by @<a href="https://twitter.com/MrPetovan" class="userinfo mention" title="MrPetovan">MrPetovan</a></blockquote><sup><a href="https://www.eyeem.com/p/120800309">www.eyeem.com</a></sup></div> +
objectstring{"created_at":"Tue Mar 27 21:07:02 +0000 2018","id":978740198937907200,"id_str":"978740198937907200","full_text":"Over-compensation #Street #Night #CarLights #Jeep #NoPeople #Close-up https:\/\/t.co\/7w4ua13QA7","truncated":false,"display_text_range":[0,93],"entities":{"hashtags":[{"text":"Street","indices":[18,25]},{"text":"Night","indices":[26,32]},{"text":"CarLights","indices":[33,43]},{"text":"Jeep","indices":[44,49]},{"text":"NoPeople","indices":[50,59]},{"text":"Close","indices":[60,66]}],"symbols":[],"user_mentions":[],"urls":[{"url":"https:\/\/t.co\/7w4ua13QA7","expanded_url":"http:\/\/EyeEm.com\/p\/120800309","display_url":"EyeEm.com\/p\/120800309","indices":[70,93]}]},"source":"<a href=\"http:\/\/www.eyeem.com\" rel=\"nofollow\">EyeEm<\/a>","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":403748896,"id_str":"403748896","name":"\ud83d\udc30yp\ud83e\udd5ali\u271d\ufe0fe Pet\ud83e\udd5avan","screen_name":"MrPetovan","location":"NYC","description":"White male form of milquetoast. Avatar by @DearMsDear inspired by @TSG_LAB.\n\nFriendica\/Diaspora\/Mastodon: hypolite@friendica.mrpetovan.com","url":"https:\/\/t.co\/PcARi5OhQO","entities":{"url":{"urls":[{"url":"https:\/\/t.co\/PcARi5OhQO","expanded_url":"https:\/\/mrpetovan.com","display_url":"mrpetovan.com","indices":[0,23]}]},"description":{"urls":[]}},"protected":false,"followers_count":182,"friends_count":146,"listed_count":15,"created_at":"Wed Nov 02 23:13:14 +0000 2011","favourites_count":45826,"utc_offset":-14400,"time_zone":"Eastern Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":15554,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/370213187\/fond_twitter_mrpetovan.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/370213187\/fond_twitter_mrpetovan.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/968008546322395136\/6qLCiu0o_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/968008546322395136\/6qLCiu0o_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/403748896\/1464321684","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"000000","profile_use_background_image":true,"has_extended_profile":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false,"lang":"en"}
allow_cidstring
allow_gidstring
deny_cidstring
deny_gidstring
item_idnumber791875
item_networkstringdfrn
author-thumbstringhttps://friendica.mrpetovan.com/photo/0cb3d7231eb751139d7d309c7c686c49-5.png?ts=1522941604
owner-thumbstringhttps://friendica.mrpetovan.com/photo/0cb3d7231eb751139d7d309c7c686c49-5.png?ts=1522941604
networkstring
urlstringhttps://friendica.mrpetovan.com/profile/hypolite
namestringHypolite Petovan
writableboolean0
selfboolean1
cidnumber1
aliasstring
event-createddatenull
event-editeddatenull
event-startdatenull
event-finishdatenull
event-summarystringnull
event-descstringnull
event-locationstringnull
event-typestringnull
event-nofinishstringnull
event-adjustbooleannull
event-ignorebooleannull
pagedropstringtrue
tagslist +
    +
  1. #<a href="https://friendica.mrpetovan.com/search?tag=Street" target="_blank">street</a>
  2. +
  3. #<a href="https://friendica.mrpetovan.com/search?tag=Night" target="_blank">night</a>
  4. +
  5. #<a href="https://friendica.mrpetovan.com/search?tag=CarLights" target="_blank">carlights</a>
  6. +
  7. #<a href="https://friendica.mrpetovan.com/search?tag=Jeep" target="_blank">jeep</a>
  8. +
  9. #<a href="https://friendica.mrpetovan.com/search?tag=NoPeople" target="_blank">nopeople</a>
  10. +
  11. #<a href="https://friendica.mrpetovan.com/search?tag=Close" target="_blank">close</a>
  12. +
  13. @<a href="https://twitter.com/MrPetovan" target="_blank">mrpetovan</a>
  14. +
  15. #<a href="https://friendica.mrpetovan.com/search?tag=Close-up" target="_blank">close-up</a>
  16. +
+
hashtagslist +
    +
  1. #<a href="https://friendica.mrpetovan.com/search?tag=Street" target="_blank">street</a>
  2. +
  3. #<a href="https://friendica.mrpetovan.com/search?tag=Night" target="_blank">night</a>
  4. +
  5. #<a href="https://friendica.mrpetovan.com/search?tag=CarLights" target="_blank">carlights</a>
  6. +
  7. #<a href="https://friendica.mrpetovan.com/search?tag=Jeep" target="_blank">jeep</a>
  8. +
  9. #<a href="https://friendica.mrpetovan.com/search?tag=NoPeople" target="_blank">nopeople</a>
  10. +
  11. #<a href="https://friendica.mrpetovan.com/search?tag=Close" target="_blank">close</a>
  12. +
  13. #<a href="https://friendica.mrpetovan.com/search?tag=Close-up" target="_blank">close-up</a>
  14. +
+
mentionsstring +
    +
  1. @<a href="https://twitter.com/MrPetovan" target="_blank">mrpetovan</a>
  2. +
+
\ No newline at end of file diff --git a/advancedcontentfilter/lang/C/messages.po b/advancedcontentfilter/lang/C/messages.po new file mode 100644 index 000000000..74bbaeb1c --- /dev/null +++ b/advancedcontentfilter/lang/C/messages.po @@ -0,0 +1,163 @@ +# ADDON advancedcontentfilter +# Copyright (C) +# This file is distributed under the same license as the Friendica advancedcontentfilter addon package. +# +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-04-17 04:04+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: advancedcontentfilter.php:134 +#, php-format +msgid "Filtered by rule: %s" +msgstr "" + +#: advancedcontentfilter.php:147 advancedcontentfilter.php:204 +msgid "Advanced Content Filter" +msgstr "" + +#: advancedcontentfilter.php:203 +msgid "Back to Addon Settings" +msgstr "" + +#: advancedcontentfilter.php:205 +msgid "Add a Rule" +msgstr "" + +#: advancedcontentfilter.php:206 +msgid "Help" +msgstr "" + +#: advancedcontentfilter.php:207 +msgid "" +"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." +msgstr "" + +#: advancedcontentfilter.php:208 +msgid "Your rules" +msgstr "" + +#: advancedcontentfilter.php:209 +msgid "" +"You have no rules yet! Start adding one by clicking on the button above next " +"to the title." +msgstr "" + +#: advancedcontentfilter.php:210 +msgid "Disabled" +msgstr "" + +#: advancedcontentfilter.php:211 +msgid "Enabled" +msgstr "" + +#: advancedcontentfilter.php:212 +msgid "Disable this rule" +msgstr "" + +#: advancedcontentfilter.php:213 +msgid "Enable this rule" +msgstr "" + +#: advancedcontentfilter.php:214 +msgid "Edit this rule" +msgstr "" + +#: advancedcontentfilter.php:215 +msgid "Edit the rule" +msgstr "" + +#: advancedcontentfilter.php:216 +msgid "Save this rule" +msgstr "" + +#: advancedcontentfilter.php:217 +msgid "Delete this rule" +msgstr "" + +#: advancedcontentfilter.php:218 +msgid "Rule" +msgstr "" + +#: advancedcontentfilter.php:219 +msgid "Close" +msgstr "" + +#: advancedcontentfilter.php:220 +msgid "Add new rule" +msgstr "" + +#: advancedcontentfilter.php:221 +msgid "Rule Name" +msgstr "" + +#: advancedcontentfilter.php:222 +msgid "Rule Expression" +msgstr "" + +#: advancedcontentfilter.php:223 +msgid "" +"

Examples:

  • author_link == 'https://friendica.mrpetovan.com/"
    +"profile/hypolite'
  • tags
" +msgstr "" + +#: advancedcontentfilter.php:224 +msgid "Cancel" +msgstr "" + +#: advancedcontentfilter.php:290 advancedcontentfilter.php:301 +#: advancedcontentfilter.php:312 advancedcontentfilter.php:346 +#: advancedcontentfilter.php:375 advancedcontentfilter.php:396 +msgid "You must be logged in to use this method" +msgstr "" + +#: advancedcontentfilter.php:316 advancedcontentfilter.php:350 +#: advancedcontentfilter.php:379 +msgid "Invalid form security token, please refresh the page." +msgstr "" + +#: advancedcontentfilter.php:328 +msgid "The rule name and expression are required." +msgstr "" + +#: advancedcontentfilter.php:340 +msgid "Rule successfully added" +msgstr "" + +#: advancedcontentfilter.php:354 advancedcontentfilter.php:383 +msgid "Rule doesn't exist or doesn't belong to you." +msgstr "" + +#: advancedcontentfilter.php:369 +msgid "Rule successfully updated" +msgstr "" + +#: advancedcontentfilter.php:390 +msgid "Rule successfully deleted" +msgstr "" + +#: advancedcontentfilter.php:400 +msgid "Missing argument: guid." +msgstr "" + +#: advancedcontentfilter.php:406 +#, php-format +msgid "Unknown post with guid: %s" +msgstr "" + +#: src/middlewares.php:28 +msgid "Method not found" +msgstr "" diff --git a/advancedcontentfilter/src/middlewares.php b/advancedcontentfilter/src/middlewares.php new file mode 100644 index 000000000..4cc4a1556 --- /dev/null +++ b/advancedcontentfilter/src/middlewares.php @@ -0,0 +1,30 @@ +getContainer(); + +// Error handler based off https://stackoverflow.com/a/48135009/757392 +$container['errorHandler'] = function () { + return function(Psr\Http\Message\RequestInterface $request, Psr\Http\Message\ResponseInterface $response, Exception $exception) + { + $responseCode = 500; + + if (is_a($exception, 'Friendica\Network\HTTPException')) { + $responseCode = $exception->httpcode; + } + + $errors['message'] = $exception->getMessage(); + + $errors['responseCode'] = $responseCode; + + return $response + ->withStatus($responseCode) + ->withJson($errors); + }; +}; + +$container['notFoundHandler'] = function () { + return function () + { + throw new \Friendica\Network\HTTPException\NotFoundException(L10n::t('Method not found')); + }; +}; diff --git a/advancedcontentfilter/src/routes.php b/advancedcontentfilter/src/routes.php new file mode 100644 index 000000000..969ced68c --- /dev/null +++ b/advancedcontentfilter/src/routes.php @@ -0,0 +1,22 @@ +group('/advancedcontentfilter/api', function () { + /* @var $this Slim\App */ + $this->group('/rules', function () { + /* @var $this Slim\App */ + $this->get('', 'advancedcontentfilter_get_rules'); + $this->post('', 'advancedcontentfilter_post_rules'); + + $this->get('/{id}', 'advancedcontentfilter_get_rules_id'); + $this->put('/{id}', 'advancedcontentfilter_put_rules_id'); + $this->delete('/{id}', 'advancedcontentfilter_delete_rules_id'); + }); + + $this->group('/variables', function () { + /* @var $this Slim\App */ + $this->get('/{guid}', 'advancedcontentfilter_get_variables_guid'); + }); +}); diff --git a/advancedcontentfilter/templates/settings.tpl b/advancedcontentfilter/templates/settings.tpl new file mode 100644 index 000000000..51d2e406f --- /dev/null +++ b/advancedcontentfilter/templates/settings.tpl @@ -0,0 +1,99 @@ +
+ +
+

🔙 {{$backtosettings}}

+

+ {{$title}} + + + + +

+
{{$advanced_content_filter_intro}}
+

+ {{$your_rules}} + +

+
+ {{$no_rules}} +
+ +
    +
  • +

    + + + + + +

    +

    + {{$rule}} #{{ rule.id }}: {{ rule.name }} +

    +
    {{ rule.expression }}
    +
  • +
+ + + +
+
+ Show post variables +
+ + +
+ +
+
+
+{{ itemJson }}
+		
+
+ + + + + + + +