├── public
├── css
│ └── seo-hide.css
├── index.php
├── partials
│ ├── referrer-warning.php
│ └── redirect.php
├── js
│ └── seo-hide.js
└── Frontend.php
├── index.php
├── admin
├── index.php
├── partials
│ ├── logs.php
│ ├── masks.php
│ └── settings.php
├── css
│ ├── mihdan-noexternallinks-admin.min.css
│ └── mihdan-noexternallinks-admin.css
├── SiteHealth.php
├── js
│ ├── mihdan-noexternallinks-admin.min.js
│ └── mihdan-noexternallinks-admin.js
├── MaskTable.php
└── LogTable.php
├── includes
├── index.php
├── I18n.php
├── Installer.php
├── Database.php
├── Compatibility.php
├── Loader.php
├── Upgrader.php
└── Main.php
├── .github
├── FUNDING.yml
└── workflows
│ ├── deploy-ro-wp-org.yml
│ └── ci.yml
├── .wordpress-org
├── banner-16x9.png
├── banner-772x250.jpg
├── banner-772x250.png
├── icon-128x128.png
├── icon-256x256.png
├── screenshot-1.png
├── screenshot-2.png
├── screenshot-3.png
├── screenshot-4.png
├── banner-1544x500.png
├── icon-admin.svg
└── icon.svg
├── README.md
├── .gitignore
├── .distignore
├── .editorconfig
├── SECURITY.md
├── languages
└── mihdan-noexternallinks.pot
├── composer.json
├── mihdan-no-external-links.php
├── phpcs.xml
├── readme.txt
└── LICENSE
/public/css/seo-hide.css:
--------------------------------------------------------------------------------
1 | .waslinkname {
2 | text-decoration: underline;
3 | cursor: pointer;
4 | }
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
\n"
9 | "Language-Team: \n"
10 | "Language: \n"
11 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
12 | "MIME-Version: 1.0\n"
13 | "Content-Type: text/plain; charset=UTF-8\n"
14 | "Content-Transfer-Encoding: 8bit\n"
15 | "X-Generator: Loco https://localise.biz/"
16 |
17 | #. Name of the plugin
18 | msgid "Mihdan: No External Links"
19 | msgstr ""
20 |
21 | #. Description of the plugin
22 | msgid ""
23 | "Convert external links into internal links, site wide or post/page specific. "
24 | "Add NoFollow, Click logging, and more..."
25 | msgstr ""
26 |
27 | #. URI of the plugin
28 | msgid "https://wordpress.org/plugins/mihdan-no-external-links/"
29 | msgstr ""
30 |
31 | #. Author of the plugin
32 | msgid "Mikhail Kobzarev"
33 | msgstr ""
34 |
35 | #. Author URI of the plugin
36 | msgid "https://www.kobzarev.com/"
37 | msgstr ""
38 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-ro-wp-org.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to WordPress.org
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | build:
7 | name: New tag
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout code
11 | uses: actions/checkout@v4
12 |
13 | - name: Install SVN
14 | run: sudo apt-get update && sudo apt-get install -y subversion
15 |
16 | - name: Install dependencies with caching
17 | uses: ramsey/composer-install@v3
18 | with:
19 | composer-options: "--no-dev --optimize-autoloader --classmap-authoritative"
20 |
21 | - name: WordPress plugin deploy
22 | uses: 10up/action-wordpress-plugin-deploy@stable
23 | with:
24 | generate-zip: true
25 | env:
26 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
27 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
28 |
29 | - name: Upload release asset
30 | uses: softprops/action-gh-release@v2
31 | with:
32 | files: ${{github.workspace}}/${{ github.event.repository.name }}.zip
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/admin/css/mihdan-noexternallinks-admin.css:
--------------------------------------------------------------------------------
1 | input[name^="mihdan_noexternallinks_"]:read-only,
2 | textarea[name^="mihdan_noexternallinks_"]:read-only {
3 | background: rgba(255,255,255,.5);
4 | border-color: rgba(222,222,222,.75);
5 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.04);
6 | box-shadow: inset 0 1px 2px rgba(0,0,0,.04);
7 | color: rgba(51,51,51,.5);
8 | }
9 |
10 | div.list-tree,
11 | div.list-tree ul,
12 | div.list-tree li {
13 | position: relative;
14 | }
15 |
16 | div.list-tree > ul {
17 | margin: 0;
18 | padding-left: 20px;
19 | }
20 |
21 | div.list-tree li::before,
22 | div.list-tree li::after {
23 | content: "";
24 | left: -12px;
25 | position: absolute;
26 | }
27 |
28 | div.list-tree li::before {
29 | border-top: 1px solid #b4b9be;
30 | height: 0;
31 | top: 12px;
32 | width: 8px;
33 | }
34 |
35 | div.list-tree li::after {
36 | border-left: 1px solid #b4b9be;
37 | height: 120%;
38 | top: -2px;
39 | width: 0;
40 | }
41 |
42 | div.list-tree ul > li:last-child::after {
43 | height: 14px;
44 | }
45 | .mihdan_noexternallinks_hidden {
46 | display: none !important;
47 | }
48 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mihdan/mihdan-no-external-links",
3 | "description": "Convert external links into internal links, site wide or post/page specific. Add NoFollow, Click logging, and more...",
4 | "type": "wordpress-plugin",
5 | "license": "GPL-2.0-or-later",
6 | "authors": [
7 | {
8 | "name": "Mikhail Kobzarev",
9 | "email": "mikhail@kobzarev.com"
10 | }
11 | ],
12 | "minimum-stability": "dev",
13 | "prefer-stable": true,
14 | "config": {
15 | "platform": {
16 | "php": "7.4"
17 | },
18 | "allow-plugins": {
19 | "dealerdirect/phpcodesniffer-composer-installer": true
20 | }
21 | },
22 | "require": {
23 | "php": ">=7.4",
24 | "ext-openssl": "*",
25 | "phpseclib/mcrypt_compat": "^2.0",
26 | "ext-json": "*"
27 | },
28 | "require-dev": {
29 | "roave/security-advisories": "dev-latest",
30 | "squizlabs/php_codesniffer": "^3.13.5",
31 | "phpcompatibility/php-compatibility": "^9.3.5",
32 | "phpcompatibility/phpcompatibility-wp": "^2.1.8",
33 | "wp-coding-standards/wpcs": "^3.3.0"
34 | },
35 | "scripts": {
36 | "phpcs": "phpcs --standard=phpcs.xml",
37 | "phpcbf": "phpcbf --standard=phpcs.xml"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/includes/I18n.php:
--------------------------------------------------------------------------------
1 | plugin_name = $plugin_name;
40 | }
41 |
42 | /**
43 | * Load the plugin text domain for translation.
44 | *
45 | * @since 4.0.0
46 | */
47 | public function load_plugin_textdomain(): void {
48 |
49 | load_plugin_textdomain(
50 | $this->plugin_name,
51 | false,
52 | dirname( plugin_basename( __FILE__ ), 2 ) . '/languages/'
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/partials/referrer-warning.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 | plugin_name ); ?>
23 |
24 |
25 |
39 |
40 |
41 | {
4 | el.addEventListener(
5 | 'click',
6 | ( e ) => {
7 | let link = el.getAttribute( 'data-link' );
8 | let target = el.getAttribute( 'data-target' );
9 |
10 | if ( ! link ) {
11 | return;
12 | }
13 |
14 | link = decodeHTMLEntities( base64Decode( link ) );
15 |
16 | if ( target && '_blank' === target ) {
17 | w.open( link );
18 | } else {
19 | d.location.href = link;
20 | }
21 | }
22 | );
23 | }
24 | );
25 |
26 | /**
27 | * Decode HTML entities.
28 | *
29 | * Example:
30 | * - & -> &
31 | * - & -> &
32 | *
33 | * @param text String for decoding.
34 | * @returns {string}
35 | */
36 | function decodeHTMLEntities( text ) {
37 | const textArea = document.createElement( 'textarea' );
38 | textArea.innerHTML = text;
39 |
40 | return textArea.value;
41 | }
42 |
43 | function base64Decode( str ) {
44 | // Going backwards: from bytestream, to percent-encoding, to original string.
45 | return decodeURIComponent(atob(str).split('').map(function(c) {
46 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
47 | }).join(''));
48 | }
49 | } )( window, document );
50 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | cs_and_tests:
7 | strategy:
8 | matrix:
9 | os: [ubuntu-latest]
10 | php-version: [ '7.4', '8.0', '8.1' ]
11 |
12 | runs-on: ${{ matrix.os }}
13 |
14 | name: PHP ${{ matrix.php-version }} on ${{ matrix.os }}
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Install PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ matrix.php-version }}
24 | extensions: json, mysqli, mbstring, zip
25 | env:
26 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Get Composer cache directory
29 | id: composer-cache
30 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
31 |
32 | - name: Set up Composer caching
33 | uses: actions/cache@v4
34 | env:
35 | cache-name: cache-composer-dependencies
36 | with:
37 | path: ${{ steps.composer-cache.outputs.dir }}
38 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
39 | restore-keys: |
40 | ${{ runner.os }}-composer-
41 |
42 | - name: Install dependencies
43 | run: |
44 | composer config github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
45 | composer install
46 | env:
47 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
49 | - name: Run code sniffer
50 | run: composer phpcs
51 |
--------------------------------------------------------------------------------
/includes/Installer.php:
--------------------------------------------------------------------------------
1 | options_prefix = $options_prefix;
43 | }
44 |
45 | /**
46 | * Runs the installation scripts.
47 | *
48 | * @since 4.2.0
49 | */
50 | public function install(): void {
51 |
52 | $installed_version = get_option( $this->options_prefix . 'version' );
53 |
54 | if ( false === $installed_version || version_compare( $installed_version, '4.2.1', '<' ) ) {
55 |
56 | Database::migrate();
57 |
58 | $installed_version = '4.5.1';
59 | update_option( $this->options_prefix . 'version', $installed_version );
60 |
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/includes/Database.php:
--------------------------------------------------------------------------------
1 | get_charset_collate();
34 | $sql = [];
35 |
36 | if ( null === $table_name || 'external_links_logs' === $table_name ) {
37 | $new_table_name = $wpdb->prefix . 'external_links_logs';
38 | $sql[] = "CREATE TABLE $new_table_name (
39 | id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
40 | url varchar(190000) NOT NULL,
41 | referring_url varchar(190000),
42 | user_agent varchar(255),
43 | ip_address varchar(255),
44 | restricted varchar(255),
45 | date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
46 | PRIMARY KEY (id)
47 | ) $charset_collate;";
48 | }
49 |
50 | if ( null === $table_name || 'external_links_masks' === $table_name ) {
51 | $new_table_name = $wpdb->prefix . 'external_links_masks';
52 | $sql[] = "CREATE TABLE $new_table_name (
53 | id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
54 | url varchar(190000) NOT NULL,
55 | mask varchar(255) NOT NULL,
56 | short_url varchar(190000) NOT NULL,
57 | PRIMARY KEY (id)
58 | ) $charset_collate;";
59 | }
60 |
61 | return dbDelta( $sql );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/mihdan-no-external-links.php:
--------------------------------------------------------------------------------
1 | run();
59 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | A custom set of code standard rules to check for WordPress plugins.
4 |
5 |
6 | .
7 | */\.github/*
8 | */\.wordpress-org/*
9 | */languages/*
10 | */vendor/*
11 | src/WPOSA.php
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/admin/SiteHealth.php:
--------------------------------------------------------------------------------
1 | plugin_name = $plugin_name;
35 |
36 | $this->init_hooks();
37 | }
38 |
39 | /**
40 | * Init plugin hooks.
41 | */
42 | public function init_hooks(): void {
43 | add_filter( 'site_status_tests', [ $this, 'add_test' ] );
44 | }
45 |
46 | /**
47 | * Add test.
48 | *
49 | * @param array $tests Tests.
50 | *
51 | * @return array
52 | */
53 | public function add_test( $tests ): array {
54 |
55 | $tests['direct'][ MIHDAN_NO_EXTERNAL_LINKS_SLUG ] = [
56 | 'label' => __( 'Output Buffering', $this->plugin_name ),
57 | 'test' => [ $this, 'check_buffering' ],
58 | ];
59 |
60 | return $tests;
61 | }
62 |
63 | /**
64 | * Custom tests.
65 | *
66 | * @return array
67 | */
68 | public function check_buffering(): array {
69 | $output_buffer = (bool) ini_get( 'output_buffering' );
70 |
71 | // phpcs:disable WordPress.WP.I18n.NoHtmlWrappedStrings
72 |
73 | $result = [
74 | 'label' => __( 'Output Buffering is enabled', $this->plugin_name ),
75 | 'status' => 'good',
76 | 'badge' => [
77 | 'label' => __( 'Performance', $this->plugin_name ),
78 | 'color' => 'blue',
79 | ],
80 | 'description' => __( 'Output Buffering is enabled. Mask All Links will work.
', $this->plugin_name ),
81 | ];
82 |
83 | if ( false === $output_buffer ) {
84 | $result = [
85 | 'label' => __( 'Output Buffering is disabled', $this->plugin_name ),
86 | 'status' => 'critical',
87 | 'badge' => [
88 | 'label' => __( 'Performance', $this->plugin_name ),
89 | 'color' => 'red',
90 | ],
91 | 'description' => __( 'Output Buffering is disabled, Mask All Links will not work. Contact your server administrator to get this feature enabled.
', $this->plugin_name ),
92 | ];
93 | }
94 |
95 | return $result;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/includes/Compatibility.php:
--------------------------------------------------------------------------------
1 | plugin_name = $plugin_name;
48 | $this->options_prefix = $options_prefix;
49 | }
50 |
51 | /**
52 | * Checks plugin compatibility.
53 | *
54 | * Checks plugin is compatible with WordPress and PHP.
55 | * Disables plugin if checks fail.
56 | *
57 | * @since 4.0.0
58 | *
59 | * @param string $wp WordPress version.
60 | * @param string $php PHP version.
61 | *
62 | * @noinspection ForgottenDebugOutputInspection
63 | */
64 | public function check( string $wp = '3.5', string $php = '5.3' ): void {
65 |
66 | $compatibility_check = get_option( $this->options_prefix . 'compatibility_check' );
67 |
68 | if ( 1 !== $compatibility_check ) {
69 | global $wp_version;
70 |
71 | if ( version_compare( PHP_VERSION, $php, '<' ) ) {
72 | $flag = 'PHP';
73 | } elseif ( version_compare( $wp_version, $wp, '<' ) ) {
74 | $flag = 'WordPress';
75 | } else {
76 | add_option( $this->options_prefix . 'compatibility_check', 1 );
77 |
78 | return;
79 | }
80 |
81 | $version = ( 'PHP' === $flag ) ? $php : $wp;
82 |
83 | deactivate_plugins( MIHDAN_NO_EXTERNAL_LINKS_BASENAME );
84 |
85 | wp_die(
86 | 'Mihdan: No External Links ' .
87 | esc_html__( 'requires', $this->plugin_name ) . ' ' .
88 | esc_html( $flag ) . ' ' . esc_html( $version ) . ' ' .
89 | esc_html__( 'or greater', $this->plugin_name ),
90 | esc_html__( 'Plugin Activation Error', $this->plugin_name ),
91 | [
92 | 'response' => 200,
93 | 'back_link' => true,
94 | ]
95 | );
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/admin/js/mihdan-noexternallinks-admin.min.js:
--------------------------------------------------------------------------------
1 | (function($){"use strict";$(function(){var masking_type=$('input[name="mihdan_noexternallinks_masking_type"]'),redirect_time=$("input#mihdan_noexternallinks_redirect_time"),mask_links=$('input[name="mihdan_noexternallinks_mask_links"]'),mask_links_inputs=$("div.list-tree input"),link_structure=$('input[name="mihdan_noexternallinks_link_structure"]'),link_structure_default=$("input#mihdan_noexternallinks_link_structure_default"),link_structure_custom=$("input#mihdan_noexternallinks_link_structure_custom"),link_separator=$('input[name="mihdan_noexternallinks_separator"]'),link_separator_display=$(".link-separator"),link_encoding=$('input[name="mihdan_noexternallinks_link_encoding"]'),link_shortening=$('input[name="mihdan_noexternallinks_link_shortening"]'),enable_logging=$("input#mihdan_noexternallinks_logging"),log_duration=$("input#mihdan_noexternallinks_log_duration"),enable_anonymize_links=$("input#mihdan_noexternallinks_anonymize_links"),anonymous_link_provider=$("input#mihdan_noexternallinks_anonymous_link_provider"),bot_targeting=$("input#mihdan_noexternallinks_bot_targeting"),bots_selector=$("select#mihdan_noexternallinks_bots_selector"),seo_hide_mode=$('input[name="mihdan_noexternallinks_seo_hide_mode"]');seo_hide_mode.on("change",function(){$(".mihdan_noexternallinks_seo_hide_mode").toggleClass("mihdan_noexternallinks_hidden")});masking_type.on("change",function(){var masking_type_value=$(this).val();"javascript"===masking_type_value?redirect_time.prop("readonly",!1):redirect_time.prop("readonly",!0)});mask_links.on("change",function(){var mask_links_value=$(this).val();"specific"===mask_links_value?mask_links_inputs.prop("disabled",!1):mask_links_inputs.prop("disabled",!0);mask_links_inputs.prop("checked",!0)});link_separator.on("focus",function(){link_structure_default.prop("checked",0);link_structure_custom.prop("checked",1);link_shortening.prop("checked",0);link_shortening.first().prop("checked",1)});link_separator.on("keyup",function(){var separator_val=link_separator.val();link_separator_display.text(separator_val)});link_structure.on("change",function(){var link_structure_value=$(this).val();if("custom"===link_structure_value){link_shortening.prop("checked",0);link_shortening.first().prop("checked",1)}});link_structure_default.on("change",function(){link_separator.val("");link_separator_display.text("goto")});link_encoding.on("change",function(){var link_encoding_value=$(this).val();if("none"!==link_encoding_value){link_shortening.prop("checked",0);link_shortening.first().prop("checked",1)}});link_shortening.on("change",function(){link_structure_default.prop("checked",1);link_structure_custom.prop("checked",0);link_encoding.prop("checked",0);link_encoding.first().prop("checked",1);link_separator.val("");link_separator_display.text("goto")});enable_logging.on("change",function(){enable_logging.is(":checked")?log_duration.prop("readonly",!1):log_duration.prop("readonly",!0)});enable_anonymize_links.on("change",function(){enable_anonymize_links.is(":checked")?anonymous_link_provider.prop("readonly",!1):anonymous_link_provider.prop("readonly",!0)});bot_targeting.on("change",function(){var bot_targeting_value=$(this).val();"specific"===bot_targeting_value?bots_selector.prop("disabled",!1):bots_selector.prop("disabled",!0)})})})(jQuery);
--------------------------------------------------------------------------------
/admin/partials/settings.php:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
56 |
83 |
84 |
--------------------------------------------------------------------------------
/.wordpress-org/icon-admin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/admin/js/mihdan-noexternallinks-admin.js:
--------------------------------------------------------------------------------
1 | (function( $ ) {
2 | 'use strict';
3 |
4 | $(function() {
5 |
6 | var masking_type = $('input[name="mihdan_noexternallinks_masking_type"]'),
7 | redirect_time = $("input#mihdan_noexternallinks_redirect_time"),
8 | mask_links = $('input[name="mihdan_noexternallinks_mask_links"]'),
9 | mask_links_inputs = $("div.list-tree input"),
10 | link_structure = $('input[name="mihdan_noexternallinks_link_structure"]'),
11 | link_structure_default = $('input#mihdan_noexternallinks_link_structure_default'),
12 | link_structure_custom = $('input#mihdan_noexternallinks_link_structure_custom'),
13 | link_separator = $('input[name="mihdan_noexternallinks_separator"]'),
14 | link_separator_display = $('.link-separator'),
15 | link_encoding = $('input[name="mihdan_noexternallinks_link_encoding"]'),
16 | link_shortening = $('input[name="mihdan_noexternallinks_link_shortening"]'),
17 | enable_logging = $('input#mihdan_noexternallinks_logging'),
18 | log_duration = $('input#mihdan_noexternallinks_log_duration'),
19 | enable_anonymize_links = $('input#mihdan_noexternallinks_anonymize_links'),
20 | anonymous_link_provider = $('input#mihdan_noexternallinks_anonymous_link_provider'),
21 | bot_targeting = $('input#mihdan_noexternallinks_bot_targeting'),
22 | bots_selector = $('select#mihdan_noexternallinks_bots_selector'),
23 | seo_hide_mode = $('input[name="mihdan_noexternallinks_seo_hide_mode"]');
24 |
25 | seo_hide_mode.on(
26 | 'change',
27 | function () {
28 | $( '.mihdan_noexternallinks_seo_hide_mode' ).toggleClass( 'mihdan_noexternallinks_hidden' );
29 | }
30 | );
31 |
32 | masking_type.on("change", function() {
33 | var masking_type_value = $(this).val();
34 | "javascript" === masking_type_value ? redirect_time.prop("readonly", !1) : redirect_time.prop("readonly", !0)
35 | });
36 |
37 | mask_links.on("change", function() {
38 | var mask_links_value = $(this).val();
39 | "specific" === mask_links_value ? mask_links_inputs.prop("disabled", !1) : mask_links_inputs.prop("disabled", !0);
40 | mask_links_inputs.prop("checked", !0);
41 | });
42 |
43 | link_separator.on("focus", function() {
44 | link_structure_default.prop("checked", 0);
45 | link_structure_custom.prop("checked", 1);
46 |
47 | link_shortening.prop("checked", 0);
48 | link_shortening.first().prop("checked", 1);
49 | });
50 |
51 | link_separator.on("keyup", function() {
52 | var separator_val = link_separator.val();
53 | link_separator_display.text(separator_val);
54 | });
55 |
56 | link_structure.on("change", function() {
57 | var link_structure_value = $(this).val();
58 |
59 | if ("custom" === link_structure_value) {
60 | link_shortening.prop("checked", 0);
61 | link_shortening.first().prop("checked", 1);
62 | }
63 | });
64 |
65 | link_structure_default.on("change", function() {
66 | link_separator.val('');
67 | link_separator_display.text('goto');
68 | });
69 |
70 | link_encoding.on("change", function() {
71 | var link_encoding_value = $(this).val();
72 |
73 | if ("none" !== link_encoding_value) {
74 | link_shortening.prop("checked", 0);
75 | link_shortening.first().prop("checked", 1);
76 | }
77 | });
78 |
79 | link_shortening.on("change", function() {
80 | link_structure_default.prop("checked", 1);
81 | link_structure_custom.prop("checked", 0);
82 |
83 | link_encoding.prop("checked", 0);
84 | link_encoding.first().prop("checked", 1);
85 |
86 | link_separator.val('');
87 | link_separator_display.text('goto');
88 | });
89 |
90 | enable_logging.on("change", function() {
91 | enable_logging.is(':checked') ? log_duration.prop("readonly", !1) : log_duration.prop("readonly", !0);
92 | });
93 |
94 | enable_anonymize_links.on("change", function() {
95 | enable_anonymize_links.is(':checked') ? anonymous_link_provider.prop("readonly", !1) : anonymous_link_provider.prop("readonly", !0);
96 | });
97 |
98 | bot_targeting.on("change", function() {
99 | var bot_targeting_value = $(this).val();
100 | "specific" === bot_targeting_value ? bots_selector.prop("disabled", !1) : bots_selector.prop("disabled", !0);
101 | });
102 |
103 | });
104 |
105 | })( jQuery );
106 |
--------------------------------------------------------------------------------
/includes/Loader.php:
--------------------------------------------------------------------------------
1 | actions = [];
47 | $this->filters = [];
48 | }
49 |
50 | /**
51 | * Add a new action to the collection to be registered with WordPress.
52 | *
53 | * @since 4.0.0
54 | *
55 | * @param string $hook The name of the WordPress action that is being registered.
56 | * @param object $component A reference to the instance of the object on which the action is defined.
57 | * @param string $callback The name of the function definition on the $component.
58 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10.
59 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default
60 | * is 1.
61 | */
62 | public function add_action( string $hook, object $component, string $callback, int $priority = 10, int $accepted_args = 1 ): void {
63 | $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
64 | }
65 |
66 | /**
67 | * Add a new filter to the collection to be registered with WordPress.
68 | *
69 | * @since 4.0.0
70 | *
71 | * @param string $hook The name of the WordPress filter that is being registered.
72 | * @param object $component A reference to the instance of the object on which the filter is defined.
73 | * @param string $callback The name of the function definition on the $component.
74 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10.
75 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default
76 | * is 1.
77 | */
78 | public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ): void {
79 | $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
80 | }
81 |
82 | /**
83 | * A utility function that is used to register the actions and hooks into a single
84 | * collection.
85 | *
86 | * @since 4.0.0
87 | * @access private
88 | *
89 | * @param array $hooks The collection of hooks that is being registered (that is, actions or filters).
90 | * @param string $hook The name of the WordPress filter that is being registered.
91 | * @param object $component A reference to the instance of the object on which the filter is defined.
92 | * @param string $callback The name of the function definition on the $component.
93 | * @param int $priority The priority at which the function should be fired.
94 | * @param int $accepted_args The number of arguments that should be passed to the $callback.
95 | *
96 | * @return array The collection of actions and filters registered with WordPress.
97 | */
98 | private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ): array {
99 |
100 | $hooks[] = [
101 | 'hook' => $hook,
102 | 'component' => $component,
103 | 'callback' => $callback,
104 | 'priority' => $priority,
105 | 'accepted_args' => $accepted_args,
106 | ];
107 |
108 | return $hooks;
109 | }
110 |
111 | /**
112 | * Register the filters and actions with WordPress.
113 | *
114 | * @since 4.0.0
115 | */
116 | public function run(): void {
117 |
118 | foreach ( $this->filters as $hook ) {
119 | add_filter(
120 | $hook['hook'],
121 | [ $hook['component'], $hook['callback'] ],
122 | $hook['priority'],
123 | $hook['accepted_args']
124 | );
125 | }
126 |
127 | foreach ( $this->actions as $hook ) {
128 | add_action(
129 | $hook['hook'],
130 | [ $hook['component'], $hook['callback'] ],
131 | $hook['priority'],
132 | $hook['accepted_args']
133 | );
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/.wordpress-org/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
62 |
--------------------------------------------------------------------------------
/public/partials/redirect.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 | plugin_name ); ?>
23 |
84 |
85 |
86 |
87 |
88 | 0% / options->redirect_time; ?> plugin_name ); ?>
89 |
90 |
93 |
94 | options->redirect_message && $url ) {
98 | $allowed_html = [
99 | 'a' => [
100 | 'href' => true,
101 | 'title' => true,
102 | 'rel' => true,
103 | 'target' => true,
104 | 'class' => true,
105 | ],
106 | 'p' => [
107 | 'class' => true,
108 | ],
109 | 'div' => [
110 | 'class' => true,
111 | ],
112 | 'style' => [
113 | 'type' => true,
114 | ],
115 | 'script' => [
116 | 'type' => true,
117 | ],
118 | ];
119 |
120 | echo wp_kses(
121 | preg_replace(
122 | '#%linkurl%#',
123 | esc_url( $url ),
124 | $this->options->redirect_message
125 | ),
126 | $allowed_html
127 | );
128 | } elseif ( $url ) {
129 | $message = __( 'You were going to the redirect link, but something did not work properly.
Please, click ', $this->plugin_name );
130 | echo (
131 | esc_html( $message ) .
132 | '
' . esc_html__( 'HERE ', $this->plugin_name ) . '' .
133 | esc_html__( ' to go to ', $this->plugin_name ) . esc_url( $url ) . esc_html__( ' manually. ', $this->plugin_name )
134 | );
135 | } else {
136 | esc_html_e( 'Sorry, no url redirect specified. Can\'t complete request.', $this->plugin_name );
137 | }
138 | ?>
139 |
140 |
141 |
169 |
170 |
171 | plugin_name = $plugin_name;
55 | $this->options_prefix = $options_prefix;
56 |
57 | parent::__construct(
58 | [
59 | 'singular' => __( 'Mask', 'mihdan-no-external-links' ),
60 | 'plural' => __( 'Masks', 'mihdan-no-external-links' ),
61 | 'ajax' => false,
62 | ]
63 | );
64 |
65 | add_action( 'admin_notices', [ $this, 'mask_delete_notice' ] );
66 | }
67 |
68 | /**
69 | * Retrieve external links mask data from the database
70 | *
71 | * @since 4.2.0
72 | *
73 | * @param int $per_page Number of items per page.
74 | * @param int $page_number Page number.
75 | *
76 | * @return array
77 | */
78 | public function get_masks( $per_page = 5, $page_number = 1 ): array {
79 |
80 | global $wpdb;
81 |
82 | $order_by = 'id';
83 | $order = 'ASC';
84 | $offset = ( $page_number - 1 ) * $per_page;
85 | $mapping = [
86 | 'title' => 'url',
87 | 'mask' => 'mask',
88 | 'numeric' => 'id',
89 | ];
90 |
91 | // Nonce is verified in the WP_List_table class.
92 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
93 | $order = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : $order;
94 | $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : '';
95 | $order_by = array_key_exists( $orderby, $mapping ) ? $mapping[ $orderby ] : $order_by;
96 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
97 |
98 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
99 | return (array) $wpdb->get_results(
100 | $wpdb->prepare(
101 | "SELECT * FROM {$wpdb->prefix}external_links_masks ORDER BY %s %s LIMIT %d OFFSET %d",
102 | [
103 | $order_by,
104 | $order,
105 | $per_page,
106 | $offset,
107 | ]
108 | ),
109 | ARRAY_A
110 | );
111 | }
112 |
113 | /**
114 | * Delete a mask record.
115 | *
116 | * @since 4.2.0
117 | *
118 | * @param int $id Mask ID.
119 | *
120 | * @return bool
121 | */
122 | public function delete_mask( $id ): bool {
123 |
124 | global $wpdb;
125 |
126 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
127 | $delete_count = $wpdb->delete(
128 | $wpdb->prefix . 'external_links_masks',
129 | [ 'ID' => $id ],
130 | [ '%d' ]
131 | );
132 |
133 | return $delete_count > 0;
134 | }
135 |
136 | /**
137 | * Returns the count of records in the database.
138 | *
139 | * @since 4.2.0
140 | *
141 | * @return int
142 | * @noinspection SqlResolve
143 | */
144 | public function record_count(): int {
145 |
146 | global $wpdb;
147 |
148 | $table_name = $wpdb->prefix . 'external_links_masks';
149 |
150 | $sql = "SELECT COUNT(id) FROM $table_name";
151 |
152 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
153 | return (int) $wpdb->get_var( $sql );
154 | }
155 |
156 | /**
157 | * Text displayed when no customer data is available
158 | *
159 | * @since 4.2.0
160 | */
161 | public function no_items(): void {
162 |
163 | esc_html_e( 'No masks available.', $this->plugin_name );
164 | }
165 |
166 | /**
167 | * Render a column when no column specific method exists.
168 | *
169 | * @since 4.2.0
170 | *
171 | * @param array $item Item.
172 | * @param string $column_name Column name.
173 | *
174 | * @return string|null
175 | */
176 | public function column_default( $item, $column_name ): ?string {
177 |
178 | switch ( $column_name ) {
179 | case 'title':
180 | $delete_nonce = wp_create_nonce( $this->options_prefix . 'delete_mask' );
181 |
182 | $title = '' . $item['url'] . '';
183 |
184 | $page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : '';
185 | $actions = [
186 | 'delete' => sprintf(
187 | 'Delete',
188 | esc_attr( $page ),
189 | 'delete',
190 | absint( $item['id'] ),
191 | $delete_nonce
192 | ),
193 | ];
194 |
195 | return $title . $this->row_actions( $actions );
196 | case 'mask':
197 | return (string) $item['mask'];
198 | case 'numeric':
199 | return (string) $item['id'];
200 | default:
201 | break;
202 | }
203 |
204 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
205 | return (string) print_r( $item, true );
206 | }
207 |
208 | /**
209 | * Render the bulk edit checkbox
210 | *
211 | * @since 4.2.0
212 | *
213 | * @param array $item Item.
214 | *
215 | * @return string
216 | */
217 | public function column_cb( $item ): string {
218 |
219 | return sprintf(
220 | '',
221 | $item['id']
222 | );
223 | }
224 |
225 | /**
226 | * Associative array of columns
227 | *
228 | * @since 4.2.0
229 | *
230 | * @return array $columns
231 | */
232 | public function get_columns(): array {
233 |
234 | return [
235 | 'cb' => '',
236 | 'title' => __( 'URL', $this->plugin_name ),
237 | 'mask' => __( 'Mask', $this->plugin_name ),
238 | 'numeric' => __( 'Numeric', $this->plugin_name ),
239 | ];
240 | }
241 |
242 | /**
243 | * Columns to make sortable.
244 | *
245 | * @since 4.2.0
246 | *
247 | * @return array $sortable_columns
248 | */
249 | public function get_sortable_columns(): array {
250 |
251 | return [
252 | 'title' => [ 'title', true ],
253 | 'mask' => [ 'mask', true ],
254 | 'numeric' => [ 'numeric', true ],
255 | ];
256 | }
257 |
258 | /**
259 | * Returns an associative array containing the bulk action
260 | *
261 | * @since 4.2.0
262 | *
263 | * @return array $actions
264 | */
265 | public function get_bulk_actions(): array {
266 |
267 | return [ 'bulk-delete' => 'Delete' ];
268 | }
269 |
270 | /**
271 | * Handles data query and filter, sorting, and pagination.
272 | *
273 | * @since 4.2.0
274 | */
275 | public function prepare_items(): void {
276 |
277 | $this->_column_headers = $this->get_column_info();
278 |
279 | $per_page = $this->get_items_per_page( 'masks_per_page' );
280 | $current_page = $this->get_pagenum();
281 | $total_items = $this->record_count();
282 |
283 | $this->set_pagination_args(
284 | [
285 | 'total_items' => $total_items,
286 | 'per_page' => $per_page,
287 | ]
288 | );
289 |
290 | $this->items = $this->get_masks( $per_page, $current_page );
291 | }
292 |
293 | /**
294 | * Processes any bulk actions.
295 | *
296 | * @since 4.2.0
297 | */
298 | public function process_bulk_action(): void {
299 |
300 | $redirect = wp_get_raw_referer();
301 |
302 | $nonce = ! empty( $_REQUEST['_wpnonce'] )
303 | ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) )
304 | : '';
305 |
306 | if ( 'delete' === $this->current_action() && wp_verify_nonce( $nonce, $this->options_prefix . 'delete_mask' ) ) {
307 |
308 | $mask = ! empty( $_GET['mask'] ) ? absint( $_GET['mask'] ) : '';
309 | $delete = $this->delete_mask( $mask );
310 |
311 | $delete_count = 0;
312 | if ( $delete ) {
313 | ++$delete_count;
314 | }
315 |
316 | $redirect = add_query_arg( 'delete_count', $delete_count, $redirect );
317 |
318 | wp_safe_redirect( $redirect );
319 | exit;
320 | }
321 |
322 | if ( 'bulk-delete' === $this->current_action() && wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
323 |
324 | $delete_count = 0;
325 | $delete_ids = isset( $_POST['bulk-delete'] ) ?
326 | array_map( 'intval', (array) wp_unslash( $_POST['bulk-delete'] ) ) :
327 | [];
328 |
329 | foreach ( $delete_ids as $id ) {
330 | if ( $this->delete_mask( $id ) ) {
331 | ++$delete_count;
332 | }
333 | }
334 |
335 | $redirect = add_query_arg( 'delete_count', $delete_count, $redirect );
336 |
337 | wp_safe_redirect( $redirect );
338 | exit;
339 | }
340 | }
341 |
342 | /**
343 | * Display delete notice.
344 | *
345 | * @since 4.2.0
346 | */
347 | public function mask_delete_notice(): void {
348 | $delete_count = isset( $_GET['delete_count'] ) ? (int) $_GET['delete_count'] : 0;
349 |
350 | if ( 1 === $delete_count ) {
351 | ?>
352 |
353 |
354 | plugin_name
358 | );
359 | ?>
360 |
361 |
362 | 1 ) {
364 | ?>
365 |
366 |
367 | plugin_name ),
372 | number_format_i18n( $delete_count )
373 | )
374 | );
375 | ?>
376 |
377 |
378 | plugin_name = $plugin_name;
55 | $this->options_prefix = $options_prefix;
56 |
57 | parent::__construct(
58 | [
59 | 'singular' => __( 'Log', $this->plugin_name ),
60 | 'plural' => __( 'Logs', $this->plugin_name ),
61 | 'ajax' => false,
62 | ]
63 | );
64 |
65 | add_action( 'admin_notices', [ $this, 'log_delete_notice' ] );
66 | }
67 |
68 | /**
69 | * Text displayed when no customer data is available
70 | *
71 | * @since 4.0.0
72 | */
73 | public function no_items(): void {
74 | esc_html_e( 'No logs available.', $this->plugin_name );
75 | }
76 |
77 | /**
78 | * Render a column when no column specific method exists.
79 | *
80 | * @since 4.0.0
81 | *
82 | * @param array $item Item array.
83 | * @param string $column_name Column name.
84 | *
85 | * @return string|null
86 | */
87 | public function column_default( $item, $column_name ): ?string {
88 |
89 | // Nonce is verified in the WP_List_Table class.
90 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
91 | switch ( $column_name ) {
92 | case 'title':
93 | $delete_nonce = wp_create_nonce( $this->options_prefix . 'delete_log' );
94 |
95 | $title = '' . $item['url'] . '';
96 |
97 | $actions = [
98 | 'delete' => sprintf(
99 | 'Delete',
100 | isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : 1,
101 | 'delete',
102 | absint( $item['id'] ),
103 | $delete_nonce
104 | ),
105 | ];
106 |
107 | return $title . $this->row_actions( $actions );
108 | case 'referring_url':
109 | return $item['referring_url'];
110 | case 'user_agent':
111 | return $item['user_agent'];
112 | case 'ip_address':
113 | return $item['ip_address'];
114 | case 'datetime':
115 | return $item['date'];
116 | default:
117 | break;
118 | }
119 | // phpcs:enable WordPress.Security.NonceVerification.Recommended
120 |
121 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
122 | return (string) print_r( $item, true );
123 | }
124 |
125 | /**
126 | * Render the bulk edit checkbox
127 | *
128 | * @since 4.0.0
129 | *
130 | * @param array $item Item array.
131 | *
132 | * @return string
133 | */
134 | public function column_cb( $item ): string {
135 | return sprintf(
136 | '',
137 | $item['id']
138 | );
139 | }
140 |
141 | /**
142 | * Associative array of columns
143 | *
144 | * @since 4.0.0
145 | * @return array $columns
146 | */
147 | public function get_columns(): array {
148 | return [
149 | 'cb' => '',
150 | 'title' => __( 'URL', $this->plugin_name ),
151 | 'referring_url' => __( 'Referring URL', $this->plugin_name ),
152 | 'user_agent' => __( 'User Agent', $this->plugin_name ),
153 | 'ip_address' => __( 'IP Address', $this->plugin_name ),
154 | 'datetime' => __( 'Date/Time', $this->plugin_name ),
155 | ];
156 | }
157 |
158 | /**
159 | * Columns to make sortable.
160 | *
161 | * @since 4.0.0
162 | * @return array $sortable_columns
163 | */
164 | public function get_sortable_columns(): array {
165 | return [
166 | 'title' => [ 'title', true ],
167 | 'referring_url' => [ 'referring_url', true ],
168 | 'user_agent' => [ 'user_agent', true ],
169 | 'ip_address' => [ 'ip_address', true ],
170 | 'datetime' => [ 'datetime', true ],
171 | ];
172 | }
173 |
174 | /**
175 | * Returns an associative array containing the bulk action
176 | *
177 | * @since 4.0.0
178 | * @return array $actions
179 | */
180 | public function get_bulk_actions(): array {
181 | return [ 'bulk-delete' => 'Delete' ];
182 | }
183 |
184 | /**
185 | * Handles data query and filter, sorting, and pagination.
186 | *
187 | * @since 4.0.0
188 | */
189 | public function prepare_items(): void {
190 |
191 | $this->_column_headers = $this->get_column_info();
192 |
193 | $per_page = $this->get_items_per_page( 'logs_per_page' );
194 | $current_page = $this->get_pagenum();
195 | $total_items = $this->record_count();
196 |
197 | $this->set_pagination_args(
198 | [
199 | 'total_items' => $total_items,
200 | 'per_page' => $per_page,
201 | ]
202 | );
203 |
204 | $this->items = $this->get_logs( $per_page, $current_page );
205 | }
206 |
207 | /**
208 | * Returns the count of records in the database.
209 | *
210 | * @since 4.0.0
211 | *
212 | * @return int
213 | * @noinspection SqlResolve
214 | */
215 | public function record_count(): int {
216 | global $wpdb;
217 |
218 | $table_name = $wpdb->prefix . 'external_links_logs';
219 |
220 | $sql = "SELECT COUNT(id) FROM $table_name";
221 |
222 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
223 | return (int) $wpdb->get_var( $sql );
224 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
225 | }
226 |
227 | /**
228 | * Retrieve external links log data from the database
229 | *
230 | * @since 4.0.0
231 | *
232 | * @param int $per_page Items per page.
233 | * @param int $page_number Page number.
234 | *
235 | * @return array
236 | */
237 | public function get_logs( int $per_page = 5, int $page_number = 1 ): array {
238 |
239 | global $wpdb;
240 |
241 | $order_by = 'date';
242 | $order = 'DESC';
243 | $offset = ( $page_number - 1 ) * $per_page;
244 | $mapping = [
245 | 'title' => 'url',
246 | 'datetime' => 'date',
247 | 'referring_url' => 'referring_url',
248 | 'user_agent' => 'user_agent',
249 | 'ip_address' => 'ip_address',
250 | ];
251 |
252 | // Nonce is verified in the WP_List_Table class.
253 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
254 | if ( ! empty( $_REQUEST['order'] ) ) {
255 | $order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) );
256 | }
257 |
258 | if ( ! empty( $_REQUEST['orderby'] ) && array_key_exists( sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ), $mapping ) ) {
259 | $order_by = $mapping[ sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) ];
260 | }
261 | // phpcs:enable WordPress.Security.NonceVerification.Recommended
262 |
263 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
264 | return (array) $wpdb->get_results(
265 | $wpdb->prepare(
266 | "SELECT * FROM {$wpdb->prefix}external_links_logs ORDER BY %s %s LIMIT %d OFFSET %d",
267 | [
268 | $order_by,
269 | $order,
270 | $per_page,
271 | $offset,
272 | ]
273 | ),
274 | ARRAY_A
275 | );
276 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
277 | }
278 |
279 | /**
280 | * Processes any bulk actions.
281 | *
282 | * @since 4.0.0
283 | * @noinspection ForgottenDebugOutputInspection
284 | */
285 | public function process_bulk_action(): void {
286 |
287 | $redirect = wp_get_raw_referer();
288 |
289 | $nonce = ! empty( $_REQUEST['_wpnonce'] )
290 | ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) )
291 | : '';
292 |
293 | if ( 'delete' === $this->current_action() && wp_verify_nonce( $nonce, $this->options_prefix . 'delete_log' ) ) {
294 |
295 | $delete_count = 0;
296 |
297 | if ( $this->delete_log( ! empty( $_GET['log'] ) ? absint( $_GET['log'] ) : 0 ) ) {
298 | ++$delete_count;
299 | }
300 |
301 | $redirect = add_query_arg( 'delete_count', $delete_count, $redirect );
302 |
303 | wp_safe_redirect( $redirect );
304 | exit;
305 | }
306 |
307 | if ( 'bulk-delete' === $this->current_action() && wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
308 |
309 | $delete_ids = ! empty( $_POST['bulk-delete'] )
310 | ? array_map( 'intval', wp_unslash( $_POST['bulk-delete'] ) )
311 | : [];
312 |
313 | $delete_count = 0;
314 | foreach ( $delete_ids as $id ) {
315 | $delete = $this->delete_log( $id );
316 |
317 | if ( $delete ) {
318 | ++$delete_count;
319 | }
320 | }
321 |
322 | $redirect = add_query_arg( 'delete_count', $delete_count, $redirect );
323 |
324 | wp_safe_redirect( $redirect );
325 | exit;
326 | }
327 | }
328 |
329 | /**
330 | * Delete a log record.
331 | *
332 | * @since 4.0.0
333 | *
334 | * @param int $id log ID.
335 | *
336 | * @return bool
337 | */
338 | public function delete_log( int $id ): bool {
339 |
340 | global $wpdb;
341 |
342 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
343 | $delete_count = $wpdb->delete(
344 | $wpdb->prefix . 'external_links_logs',
345 | [ 'ID' => $id ],
346 | [ '%d' ]
347 | );
348 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
349 |
350 | return $delete_count > 0;
351 | }
352 |
353 | /**
354 | * Display delete notice.
355 | *
356 | * @since 4.2.0
357 | */
358 | public function log_delete_notice(): void {
359 | // Nonce is verified in the WP_List_Table class.
360 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
361 | $delete_count = ! empty( $_GET['delete_count'] ) ? (int) $_GET['delete_count'] : 0;
362 | // phpcs:enable WordPress.Security.NonceVerification.Recommended
363 |
364 | if ( 1 === $delete_count ) {
365 | ?>
366 |
367 |
368 | plugin_name
372 | );
373 | ?>
374 |
375 |
376 | 1 ) {
378 | ?>
379 |
380 |
381 | plugin_name ), $delete_count ) );
384 | ?>
385 |
386 |
387 | plugin_name = $plugin_name;
54 | $this->options_prefix = $options_prefix;
55 | }
56 |
57 | /**
58 | * Runs the upgrade scripts.
59 | *
60 | * Updates database tables, fields, and data.
61 | *
62 | * @since 4.0.0
63 | * @noinspection PhpUndefinedClassInspection
64 | */
65 | public function upgrade(): void {
66 |
67 | global $wpdb;
68 |
69 | $installed_version = get_option( $this->options_prefix . 'version' );
70 |
71 | if ( false === $installed_version || version_compare( $installed_version, '4.0.0', '<' ) ) {
72 |
73 | $current_options = get_option( 'Main' );
74 |
75 | if ( false !== $current_options ) {
76 |
77 | if ( isset( $current_options['no302'] ) && '1' === $current_options['no302'] ) {
78 | update_option( $this->options_prefix . 'masking_type', 'javascript' );
79 | }
80 |
81 | if ( isset( $current_options['disable_mask_links'] ) && '1' === $current_options['disable_mask_links'] ) {
82 | update_option( $this->options_prefix . 'masking_type', 'no' );
83 | }
84 |
85 | if ( isset( $current_options['redtime'] ) ) {
86 | update_option( $this->options_prefix . 'redirect_time', $current_options['redtime'] );
87 | }
88 |
89 | if ( isset( $current_options['mask_mine'] ) ) {
90 | if ( '1' === $current_options['mask_mine'] ) {
91 | update_option( $this->options_prefix . 'mask_links', 'specific' );
92 | update_option( $this->options_prefix . 'mask_posts_pages', true );
93 | } else {
94 | update_option( $this->options_prefix . 'mask_posts_pages', '' );
95 | }
96 | }
97 |
98 | if ( isset( $current_options['mask_comment'] ) ) {
99 | if ( '1' === $current_options['mask_comment'] ) {
100 | update_option( $this->options_prefix . 'mask_links', 'specific' );
101 | update_option( $this->options_prefix . 'mask_comments', true );
102 | } else {
103 | update_option( $this->options_prefix . 'mask_comments', '' );
104 | }
105 | }
106 |
107 | if ( isset( $current_options['mask_author'] ) ) {
108 | if ( '1' === $current_options['mask_author'] ) {
109 | update_option( $this->options_prefix . 'mask_links', 'specific' );
110 | update_option( $this->options_prefix . 'mask_comment_author', true );
111 | } else {
112 | update_option( $this->options_prefix . 'mask_comment_author', '' );
113 | }
114 | }
115 |
116 | if ( isset( $current_options['mask_rss'] ) ) {
117 | if ( '1' === $current_options['mask_rss'] ) {
118 | update_option( $this->options_prefix . 'mask_links', 'specific' );
119 | update_option( $this->options_prefix . 'mask_rss', true );
120 | } else {
121 | update_option( $this->options_prefix . 'mask_rss', '' );
122 | }
123 | }
124 |
125 | if ( isset( $current_options['mask_rss_comment'] ) ) {
126 | if ( '1' === $current_options['mask_rss_comment'] ) {
127 | update_option( $this->options_prefix . 'mask_links', 'specific' );
128 | update_option( $this->options_prefix . 'mask_rss_comments', true );
129 | } else {
130 | update_option( $this->options_prefix . 'mask_rss_comments', '' );
131 | }
132 | }
133 |
134 | if ( isset( $current_options['fullmask'] ) && '1' === $current_options['fullmask'] ) {
135 | update_option( $this->options_prefix . 'mask_links', 'all' );
136 | }
137 |
138 | if ( isset( $current_options['add_nofollow'] ) ) {
139 | if ( '1' === $current_options['add_nofollow'] ) {
140 | update_option( $this->options_prefix . 'nofollow', true );
141 | } else {
142 | update_option( $this->options_prefix . 'nofollow', '' );
143 | }
144 | }
145 |
146 | if ( isset( $current_options['add_blank'] ) ) {
147 | if ( '1' === $current_options['add_blank'] ) {
148 | update_option( $this->options_prefix . 'target_blank', true );
149 | } else {
150 | update_option( $this->options_prefix . 'target_blank', '' );
151 | }
152 | }
153 |
154 | if ( isset( $current_options['put_noindex'] ) ) {
155 | if ( '1' === $current_options['put_noindex'] ) {
156 | update_option( $this->options_prefix . 'noindex_tag', true );
157 | } else {
158 | update_option( $this->options_prefix . 'noindex_tag', '' );
159 | }
160 | }
161 |
162 | if ( isset( $current_options['put_noindex_comment'] ) ) {
163 | if ( '1' === $current_options['put_noindex_comment'] ) {
164 | update_option( $this->options_prefix . 'noindex_comment', true );
165 | } else {
166 | update_option( $this->options_prefix . 'noindex_comment', '' );
167 | }
168 | }
169 |
170 | if ( isset( $current_options['LINK_SEP'] ) && 'goto' !== $current_options['LINK_SEP'] ) {
171 | update_option( $this->options_prefix . 'link_structure', 'custom' );
172 | update_option( $this->options_prefix . 'separator', $current_options['LINK_SEP'] );
173 | } else {
174 | update_option( $this->options_prefix . 'link_structure', 'default' );
175 | update_option( $this->options_prefix . 'separator', '' );
176 | }
177 |
178 | if ( isset( $current_options['maskurl'] ) && '1' === $current_options['maskurl'] ) {
179 | if ( isset( $current_options['base64'] ) && '1' === $current_options['base64'] ) {
180 | update_option( $this->options_prefix . 'link_encoding', 'none' );
181 | } else {
182 | update_option( $this->options_prefix . 'link_encoding', 'numbers' );
183 | }
184 | }
185 |
186 | if ( isset( $current_options['base64'] ) && '1' === $current_options['base64'] ) {
187 | if ( isset( $current_options['maskurl'] ) && '1' === $current_options['maskurl'] ) {
188 | update_option( $this->options_prefix . 'link_encoding', 'none' );
189 | } else {
190 | update_option( $this->options_prefix . 'link_encoding', 'base64' );
191 | }
192 | }
193 |
194 | if ( isset( $current_options['stats'] ) ) {
195 | if ( '1' === $current_options['stats'] ) {
196 | update_option( $this->options_prefix . 'logging', true );
197 | } else {
198 | update_option( $this->options_prefix . 'logging', '' );
199 | }
200 | }
201 |
202 | if ( isset( $current_options['keep_stats'] ) ) {
203 | update_option( $this->options_prefix . 'log_duration', $current_options['keep_stats'] );
204 | }
205 |
206 | if ( isset( $current_options['restrict_referer'] ) ) {
207 | if ( '1' === $current_options['restrict_referer'] ) {
208 | update_option( $this->options_prefix . 'check_referrer', true );
209 | } else {
210 | update_option( $this->options_prefix . 'check_referrer', '' );
211 | }
212 | }
213 |
214 | if ( isset( $current_options['remove_links'] ) ) {
215 | if ( '1' === $current_options['remove_links'] ) {
216 | update_option( $this->options_prefix . 'remove_all_links', true );
217 | } else {
218 | update_option( $this->options_prefix . 'remove_all_links', '' );
219 | }
220 | }
221 |
222 | if ( isset( $current_options['link2text'] ) ) {
223 | if ( '1' === $current_options['link2text'] ) {
224 | update_option( $this->options_prefix . 'links_to_text', true );
225 | } else {
226 | update_option( $this->options_prefix . 'links_to_text', '' );
227 | }
228 | }
229 |
230 | if ( isset( $current_options['debug'] ) ) {
231 | if ( '1' === $current_options['debug'] ) {
232 | update_option( $this->options_prefix . 'debug_mode', true );
233 | } else {
234 | update_option( $this->options_prefix . 'debug_mode', '' );
235 | }
236 | }
237 |
238 | if (
239 | isset( $current_options['redtxt'] ) &&
240 | 'This page demonstrates link redirect with "WP-NoExternalLinks" plugin. You will be redirected in 3 seconds. Otherwise, please click on this link.' !== $current_options['redtxt']
241 | ) {
242 | $redirect_text = str_replace( 'LINKURL', '%linkurl%', $current_options['redtxt'] );
243 |
244 | update_option( $this->options_prefix . 'redirect_message', $redirect_text );
245 | }
246 |
247 | if ( isset( $current_options['exclude_links'] ) && '' !== $current_options['exclude_links'] && '0' !== $current_options['exclude_links'] ) {
248 | update_option( $this->options_prefix . 'exclusion_list', $current_options['exclude_links'] );
249 | }
250 |
251 | if ( isset( $current_options['noforauth'] ) ) {
252 | if ( '1' === $current_options['noforauth'] ) {
253 | update_option( $this->options_prefix . 'skip_auth', true );
254 | } else {
255 | update_option( $this->options_prefix . 'skip_auth', '' );
256 | }
257 | }
258 |
259 | if ( isset( $current_options['dont_mask_admin_follow'] ) ) {
260 | if ( '1' === $current_options['dont_mask_admin_follow'] ) {
261 | update_option( $this->options_prefix . 'skip_follow', true );
262 | } else {
263 | update_option( $this->options_prefix . 'skip_follow', '' );
264 | }
265 | }
266 |
267 | if ( false !== get_option( 'mihdan_noexternallinks_flush' ) ) {
268 | update_option(
269 | $this->options_prefix . 'last_cleared_logs',
270 | get_option( 'mihdan_noexternallinks_flush' )
271 | );
272 | }
273 |
274 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
275 | // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
276 | $wpdb->update(
277 | $wpdb->prefix . 'postmeta',
278 | [ 'meta_value' => 'default' ],
279 | [
280 | 'meta_key' => 'mihdan_noextrenallinks_mask_links',
281 | 'meta_value' => 0,
282 | ]
283 | );
284 |
285 | $wpdb->update(
286 | $wpdb->prefix . 'postmeta',
287 | [ 'meta_value' => 'disabled' ],
288 | [
289 | 'meta_key' => 'mihdan_noextrenallinks_mask_links',
290 | 'meta_value' => 2,
291 | ]
292 | );
293 |
294 | $wpdb->update(
295 | $wpdb->prefix . 'postmeta',
296 | [ 'meta_key' => 'mask_links' ],
297 | [ 'meta_key' => 'mihdan_noextrenallinks_mask_links' ]
298 | );
299 | // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
300 |
301 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
302 | $table_name = $wpdb->prefix . 'links_stats';
303 | $new_table_name = $wpdb->prefix . 'external_links_logs';
304 | $wpdb->query( "ALTER TABLE $table_name RENAME $new_table_name" );
305 | $wpdb->query( "DROP TABLE IF EXISTS $table_name" );
306 |
307 | $table_name = $wpdb->prefix . 'masklinks';
308 | $new_table_name = $wpdb->prefix . 'external_links_masks';
309 | $wpdb->query( "ALTER TABLE $table_name RENAME $new_table_name" );
310 | $wpdb->query( "DROP TABLE IF EXISTS $table_name" );
311 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
312 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
313 |
314 | delete_option( 'Main' );
315 | delete_option( 'mihdan_noexternallinks_flush' );
316 |
317 | $installed_version = '4.0.0';
318 | update_option( $this->options_prefix . 'version', $installed_version );
319 |
320 | add_action( 'admin_notices', [ $this, 'update_notice' ] );
321 |
322 | }
323 | }
324 |
325 | if ( '4.0.0' === $installed_version ) {
326 |
327 | Mihdan_NoExternalLinks_Database::migrate();
328 |
329 | $installed_version = '4.2.0';
330 | update_option( $this->options_prefix . 'version', $installed_version );
331 |
332 | }
333 |
334 | if ( version_compare( $installed_version, '4.2.0', '<=' ) ) {
335 |
336 | Mihdan_NoExternalLinks_Database::migrate();
337 |
338 | $installed_version = '4.5.1';
339 | update_option( $this->options_prefix . 'version', $installed_version );
340 |
341 | }
342 | }
343 |
344 | /**
345 | * Display update notice.
346 | *
347 | * @since 4.0.0
348 | */
349 | public function update_notice(): void {
350 | ?>
351 |
352 |
353 | plugin_name
358 | );
359 | ?>
360 |
361 |
362 | Add New'
54 | 2. Search for 'No External Links'
55 | 3. Activate No External Links from your Plugins page.
56 | 4. [Optional] Configure No External Links settings.
57 |
58 | = From WordPress.org =
59 | 1. Download No External Links.
60 | 2. Upload the 'mihdan-noexternallinks' directory to your '/wp-content/plugins/' directory, using your favorite method (ftp, sftp, scp, etc...)
61 | 3. Activate No External Links from your Plugins page.
62 | 4. [Optional] Configure No External Links settings.
63 |
64 | == Frequently Asked Questions ==
65 |
66 | = How can I report security bugs? =
67 |
68 | You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team helps validate, triage and handle any security vulnerabilities. [Report a security vulnerability.](https://patchstack.com/database/vdp/9e5fb8ab-3bfb-4d24-9b50-c3f6c7512f1a)
69 |
70 | == Changelog ==
71 |
72 | = 5.1.8 (29.11.2025) =
73 | * Resolved #[42](https://github.com/mihdan/mihdan-no-external-links/issues/42)
74 |
75 | = 5.1.7 (01.10.2025) =
76 | * Resolved #[41](https://github.com/mihdan/mihdan-no-external-links/issues/41)
77 |
78 | = 5.1.6 (28.09.2025) =
79 | * Resolved #[39](https://github.com/mihdan/mihdan-no-external-links/issues/39)
80 | * Resolved #[40](https://github.com/mihdan/mihdan-no-external-links/issues/40)
81 | * The error "Function _load_textdomain_just_in_time was called incorrectly" has been fixed
82 |
83 | = 5.1.5 (28.09.2025) =
84 | * Tested with WordPress 6.4+
85 |
86 | = 5.1.4 (02.06.2024) =
87 | * Fixed error of saving include/exclude sections
88 |
89 | = 5.1.3 (21.05.2024) =
90 | * Fixed error of decoding links hidden with SeoHide
91 | * Fixed a fatal error related to the `Mihdan_NoExternalLinks_Database` class
92 |
93 | = 5.1.2 (08.12.2023) =
94 | * Added a code editor to modify the redirect page
95 | * Added progress bar to redirect page
96 | * Fixed display error when an custom page for redirect is specified
97 |
98 | = 5.1.1 (06.12.2023) =
99 | * Fixed fatal errors on admin pages
100 | * Fixed bugs in SEO Hide module
101 |
102 | = 5.1.0 (25.11.2023) =
103 | * Tested with WordPress 6.4+.
104 | * Added PHP 8.3 support
105 | * Added excluded list for SEO hide
106 |
107 | = 5.0.7 (03.05.2023) =
108 | * Fixed ampersand conversion in links
109 | * Fixed PHP notices
110 |
111 | = 5.0.6 (02.05.2023) =
112 | * Resolve #[20](https://github.com/mihdan/mihdan-no-external-links/issues/20)
113 | * Resolve #[25](https://github.com/mihdan/mihdan-no-external-links/issues/25)
114 | * Resolve #[30](https://github.com/mihdan/mihdan-no-external-links/issues/30)
115 |
116 | = 5.0.5 (18.04.2023) =
117 | * Tested with WordPress 6.2.
118 | * Added PHP 8.2 support
119 | * Removed settings for the dead "Link Shrink" service
120 | * Removed custom link parser functionality
121 |
122 | = 5.0.4 (13.09.2022) =
123 | * Added possibility to specify any page of the site as a redirect page
124 |
125 | = 5.0.3 (12.09.2022) =
126 | * Fixed fatal errors on logging pages
127 | * Fixed errors in translations in the WordPress admin
128 |
129 | = 5.0.2 (09.09.2022) =
130 | * Added polyfill for the deprecated mcrypt PHP module
131 |
132 | = 5.0.1 (08.09.2022) =
133 | * Code refactoring to conform coding and security standards.
134 |
135 | = 5.0.0 (14.04.2022) =
136 | * Code refactoring
137 | * Set minimum compatibility PHP version to 7.4
138 | * PHP 8.1 compatibility
139 |
140 | = 4.8.0 (07.04.2022) =
141 | * Added a link to plugin settings on all plugins page
142 | * Fixed security issues
143 |
144 | = 4.7.4 (13.12.2022) =
145 | * Fixed a bug with SEO hide
146 |
147 | = 4.7.3 (07.09.2021) =
148 | * Check if `wp_referer()` exists to work properly with `rel="noreferrer"` links
149 |
150 | = 4.7.2 (05.04.2021) =
151 | * Added a new settings page with all the author's plugins
152 |
153 | = 4.7.1 (04.04.2021) =
154 | * Fixed a bug with SEO hide
155 | * Fixed a bug with the name of the main plugin file
156 |
157 | = 4.7.0 (03.04.2021) =
158 | * Hiding links using SEO hide method
159 |
160 | = 4.6.0 (02.04.2021) =
161 | * Added support for WordPress 5.7
162 | * Added support for HTTP status codes (301, 302, 307)
163 | * Added script for auto deploy to wp.org
164 |
165 | = 4.5.2 (30.04.2020) =
166 | * Added support for WordPress 5.4
167 | * Fixed bug with headers
168 |
169 | = 4.5.1 (19.01.2020) =
170 | * Added output buffering test for Site Health
171 | * Added domain & advert type fields for adf.ly
172 | * Added domain *.wordpress.org to exclude list
173 | * Fixed bug with output buffer notice
174 | * Fixed bug with & and & in URL
175 | * Fixed bug with adf.ly support
176 | * Updated referrer-warning.php template
177 | * Updated referrer.php template
178 | * Updated yourls support
179 | * Updated layout for links settings page
180 | * Prevented error 404 on redirect page
181 |
182 | = 4.5 (2019-04-16) =
183 | * Removed possible SQL injection in admin panel scripts
184 |
185 | = 4.4 (2019-04-12) =
186 | * RegEx pattern to DomDocument
187 | * WPCS
188 | * Fix bug on log page
189 |
190 | = 4.3.8 (2019-02-02) =
191 | * Built-in support for yourls link shortening
192 |
193 | = 4.3.7 (2019-01-31) =
194 | * Fix bug with cyr2lat plugin: remove `sanitize_title` from menu items
195 |
196 | = 4.3.6 (2018-12-07) =
197 | * Add assets for plugin
198 |
199 | = 4.3.5 (2018-08-09) =
200 | * Fix bug with `Mihdan_NoExternalLinks_Admin_Log_Table`
201 |
202 | = 4.3.4 (2018-08-08) =
203 | * Added new lines to the translation
204 |
205 | = 4.3.3 (2018-08-08) =
206 | * Change text domain from `mihdan-noexternallinks` to `mihdan-no-external-links`
207 |
208 | = 4.3.2 =
209 | * Update README.txt
210 |
211 | = 4.3.1 =
212 | * Bump version
213 |
214 | = 4.3 =
215 | * Forked from https://wordpress.org/plugins/wp-noexternallinks/
216 |
217 | = 4.2.2 =
218 | Several bug fixes.
219 |
220 | = 4.2.1 =
221 | Fixed several PHP Compatibility issues.
222 |
223 | = 4.2.0 =
224 | Added many new features including:
225 | * AES-256 Link Encryption
226 | * Link Anonymizer
227 | * Domain Specific Targeting
228 | * Bot Specific Targeting
229 | * Improved Logging functionality
230 | * Built-in support for link shortening (Adf.ly, Bitly, Link Shrink, Shorte.st)
231 |
232 | And several bug fixes.
233 |
234 | = 4.1.0 =
235 | Re-coded Custom Parser functionality.
236 |
237 | = 4.0.2 =
238 | Fixed a PHP Compatibility issue.
239 |
240 | = 4.0.1 =
241 | Bug fixes.
242 |
243 | = 4.0.0 =
244 | Plugin rewritten. Major overhaul of the code base.
245 |
246 | = 3.5.9.9 =
247 | Added custom filter with name "wp_noexternallinks". Please use it for custom fields and so on.
248 |
249 | = 3.5.9.8 =
250 | Fixed custom parser load.
251 |
252 | = 3.5.9.7 =
253 | Add support for custom location of wp-content dir.
254 |
255 | = 3.5.9.6 =
256 | Fix for RSS masking.
257 |
258 | = 3.5.9.5 =
259 | Added support for relative links, beginning with slash (/).
260 |
261 | = 3.5.9.4 =
262 | Added masking options for RSS feed.
263 |
264 | = 3.5.9.3 =
265 | Added noindex comment option for yandex search engine.
266 |
267 | = 3.5.9.2 =
268 | Parser logic optimization and fixes.
269 |
270 | = 3.5.9.10 =
271 | Disabled full masking when running cron job to avoid collisions.
272 |
273 | = 3.5.9.1 =
274 | Fixed bug when statistic was not written.
275 |
276 | = 3.5.9 =
277 | Updated filter to support multiline links code.
278 |
279 | = 3.5.8 =
280 | Custom parser file moved to uploads directory to avoid deletion.
281 |
282 | = 3.5.7 =
283 | Custom parser file moved to uploads directory to avoid deletion.
284 |
285 | = 3.5.6 =
286 | Fixed bug with writing click stats to database.
287 |
288 | = 3.5.5 =
289 | Divided code to smaller functions for easier overwrite with custom modes.
290 |
291 | = 3.5.4 =
292 | Fixed "rel=follow" feature. Added icon for admin menu.
293 |
294 | = 3.5.3 =
295 | Do not disable error reporting on server any more.
296 |
297 | = 3.5.20 =
298 | minor text fixes
299 |
300 | = 3.5.2 =
301 | Some refactoring.
302 |
303 | = 3.5.19 =
304 | minor XSS fix (thanks to DefenseCode WebScanner), more debug, fix possible bug with numeric masking
305 |
306 | = 3.5.18 =
307 | added index on links table
308 |
309 | = 3.5.17 =
310 | fix for better compatibility with php7
311 |
312 | = 3.5.16 =
313 | minor security fix
314 |
315 | = 3.5.15 =
316 | fix masking issues with mixed http/https
317 |
318 | = 3.5.14 =
319 | fallback to 3.5.11
320 |
321 | = 3.5.13 =
322 | bugged versions
323 |
324 | = 3.5.12 =
325 | bugged versions
326 |
327 | = 3.5.11 =
328 | minor improvements
329 |
330 | = 3.5.10 =
331 | Fixed issues with cron job
332 |
333 | = 3.5.1 =
334 | Added option for developers - now you can extend plugin with custom parsing functions! Just rename "custom-parser.sample.php" to "custom-parser.php" and extend the class (see sample file for details). Your modifications will stay even after plugin upgrade!
335 |
336 | = 3.5 =
337 | Redesigned user friendly admin area!
338 |
339 | = 3.4.5 =
340 | Added option to disable links masking when link is made by admin and has **rel="follow"** attribute
341 |
342 | = 3.4.4 =
343 | Added exclusion for polugin from WP Super Cache.
344 |
345 | = 3.4.3 =
346 | Added detection and prevention of possible spoofing attacks. See new option in plugin settings. It is enabled by default.
347 |
348 | = 3.4.2 =
349 | Fixed displaying error where there are no stats for today.
350 |
351 | = 3.4.1 =
352 | Fixed displaying error where there are no stats for today.
353 |
354 | = 3.4 =
355 | Replaced direct SQL queries with WPDB interface.
356 |
357 | = 3.3.9.2 =
358 | Now debug mode does not mess up web site. Also added some text to options page.
359 |
360 | = 3.3.9.1 =
361 | Added some more debug.
362 |
363 | = 3.3.9 =
364 | Updated for correct work with enabled statistics and Hyper Cache plugin.
365 |
366 | = 3.3.8 =
367 | Correct redirection links with GET parameters, sometimes damaged by wordpress output.
368 |
369 | = 3.3.7 =
370 | Critical update for 3.3.6.
371 |
372 | = 3.3.6 =
373 | More output for debug mode.
374 |
375 | = 3.3.5 =
376 | Little update so that plugin won't cause harmless warning.
377 |
378 | = 3.3.4 =
379 | Now you can customize link view if you chose "remove links" or "turn links into text". Use CSS classes "waslinkname" and "waslinkurl" for it.
380 |
381 | = 3.3.3b =
382 | Exclusions list fix, possible fix for not found 'is_user_logged_in' function.
383 |
384 | = 3.3.2 =
385 | Imporovements for option "Mask ALL links in document", debug mode.
386 |
387 | = 3.3.1 =
388 | Hotfix for some blogs which crashed on checking if page is RSS feed, improvements for option "Mask ALL links in document" - now it doesn'n mask RSS and posts with option "don't mask links".
389 |
390 | = 3.3 =
391 | Additional protect from masking links in RSS, fix for admin panel in wordpress 3.4.2, Perfomance fixes.
392 |
393 | = 3.2 =
394 | Two new options, little backslashes fix, error reporting fix.
395 |
396 | = 3.1.1 =
397 | Improved compatibility with some shitty servers.
398 |
399 | = 3.1.0 =
400 | Added masking links with digital short codes.
401 |
402 | = 3.0.4 =
403 | Fixed when some options in checkboxes couldn't be changed.
404 |
405 | = 3.0.3 =
406 | Removed some extra info, added some error handlers, repaired broken system for flushing click stats.
407 |
408 | = 3.0.2 =
409 | Removed test message "failed to update options" when nothing changed in options. Also, fixed issue when, if link masking was disabled for post, it was also disabled for comments.
410 |
411 | = 3.0.1 =
412 | Fixed option update issue.
413 |
414 | = 3.0.0 =
415 | Code improvements, added .po translation, clicks stats and option to mask Everything.
416 |
417 | = 2.172 =
418 | fixed javascript error when redirects ended with ";"
419 |
420 | = 2.171 =
421 | Added automatic exclusion of internal links (#smth) from masking.
422 |
423 | = 2.17 =
424 | Several bugfixes for low possible case scenarios...
425 |
426 | = 2.16 =
427 | Javascript links aren't broken by plugin now, thanks to [Andu](http://anduriell.es).
428 |
429 | = 2.15 =
430 | Fixed for some servers with setup which replaces "//" with"/".
431 |
432 | = 2.14 =
433 | Absolute file paths used now instead of relative.
434 |
435 | = 2.13 =
436 | Fixed language inclusion problem which apperared in some cases.
437 |
438 | = 2.12 =
439 | Fully compatible with PHP4.
440 |
441 | = 2.11 =
442 | Removed "public" keyword in class functions definitions. Probably will be more compatible with PHP4.
443 |
444 | = 2.10 =
445 | Plugin was rewrited for faster performance, fixed adding targer="_blank" for internal links.
446 |
447 | = 2.05 =
448 | Fixed internationalization, added Belarusian language.
449 |
450 | = 2.04 =
451 | Changed default settings, removed "disable links masking".
452 |
453 | = 2.03 =
454 | Fixed broken exclusions list.
455 |
456 | = 2.02 =
457 | Updated to execute later then other link filters, preventing possible problems with other plugins.
458 |
459 | = 2.01 =
460 | Little bugfix, for fixing errors when empty exlusions.
461 |
462 | = 2.0 =
463 | Many significant changes, including urls and post exclusion from masking, another rewrite structure, and new options.
464 |
465 | = 0.071 =
466 | Russian translation corrected.
467 |
468 | = 0.07 =
469 | Better work for sites wihout mod_rewrite.
470 |
471 | = 0.06 =
472 | Bugfix for email links.
473 |
474 | = 0.05 =
475 | Bugfix for wrong html parsing.
476 |
477 | = 0.04 =
478 | Activation/Deactivation improved, optimization, localization settings now stored as options.
479 |
480 | = 0.03 =
481 | Bugfix.
482 |
483 | = 0.02 =
484 | Multilanguagal release.
485 |
486 | = 0.01 =
487 | First release.
488 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/includes/Main.php:
--------------------------------------------------------------------------------
1 | plugin_name = 'mihdan-no-external-links';
106 | $this->version = MIHDAN_NO_EXTERNAL_LINKS_VERSION;
107 | $this->options_prefix = 'mihdan_noexternallinks_';
108 |
109 | $this->load_dependencies();
110 | $this->compatibility_check();
111 | $this->install();
112 | $this->upgrade();
113 | $this->set_locale();
114 | $this->set_options();
115 | $this->initiate();
116 | $this->define_admin_hooks();
117 | $this->define_public_hooks();
118 | }
119 |
120 | /**
121 | * Load the required dependencies for this plugin.
122 | *
123 | * Create an instance of the loader which will be used to register the hooks
124 | * with WordPress.
125 | *
126 | * @since 4.0.0
127 | * @access private
128 | */
129 | private function load_dependencies(): void {
130 |
131 | /**
132 | * The class responsible for orchestrating the actions and filters of the
133 | * core plugin.
134 | */
135 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/Loader.php';
136 |
137 | /**
138 | * The class responsible for defining internationalization functionality
139 | * of the plugin.
140 | */
141 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/I18n.php';
142 |
143 | /**
144 | * The class responsible for checking compatibility.
145 | */
146 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/Compatibility.php';
147 |
148 | /**
149 | * The class responsible for database tables.
150 | */
151 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/Database.php';
152 |
153 | /**
154 | * The class responsible for installing the plugin.
155 | */
156 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/Installer.php';
157 |
158 | /**
159 | * The class responsible for upgrading the plugin.
160 | */
161 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/includes/Upgrader.php';
162 |
163 | /**
164 | * Site Health Tests.
165 | */
166 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/admin/SiteHealth.php';
167 |
168 | /**
169 | * The class responsible for defining all actions that occur in the admin area.
170 | */
171 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/admin/Admin.php';
172 |
173 | /**
174 | * The class responsible for the masks table.
175 | */
176 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/admin/MaskTable.php';
177 |
178 | /**
179 | * The class responsible for the logs table.
180 | */
181 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/admin/LogTable.php';
182 |
183 | /**
184 | * The class responsible for defining all actions that occur in the public-facing
185 | * side of the site.
186 | */
187 | require_once MIHDAN_NO_EXTERNAL_LINKS_DIR . '/public/Frontend.php';
188 |
189 | $this->loader = new Loader();
190 | }
191 |
192 | /**
193 | * Runs the compatibility check.
194 | *
195 | * Checks if the plugin is compatible with WordPress and PHP.
196 | * Disables the plugin if checks fail.
197 | *
198 | * @since 4.0.0
199 | * @access private
200 | */
201 | private function compatibility_check(): void {
202 |
203 | $plugin_compatibility = new Compatibility( $this->get_plugin_name(), $this->get_options_prefix() );
204 |
205 | $this->loader->add_action( 'admin_init', $plugin_compatibility, 'check' );
206 | }
207 |
208 | /**
209 | * Runs the installation scripts.
210 | *
211 | * @since 4.2.0
212 | * @access private
213 | */
214 | private function install(): void {
215 |
216 | $current_options = get_option( 'Main' );
217 |
218 | if ( false === $current_options ) {
219 | $plugin_installer = new Installer(
220 | $this->get_plugin_name(),
221 | $this->get_version(),
222 | $this->get_options_prefix()
223 | );
224 |
225 | $plugin_installer->install();
226 | }
227 | }
228 |
229 | /**
230 | * Runs the upgrade scripts.
231 | *
232 | * Updates database tables, fields, and data.
233 | *
234 | * @since 4.0.0
235 | * @access private
236 | */
237 | private function upgrade(): void {
238 |
239 | $plugin_upgrader = new Upgrader(
240 | $this->get_plugin_name(),
241 | $this->get_version(),
242 | $this->get_options_prefix()
243 | );
244 |
245 | $plugin_upgrader->upgrade();
246 | }
247 |
248 | /**
249 | * Define the locale for this plugin for internationalization.
250 | *
251 | * Uses the Plugin_Name_i18n class in order to set the domain and to register the hook
252 | * with WordPress.
253 | *
254 | * @since 4.0.0
255 | * @access private
256 | */
257 | private function set_locale(): void {
258 |
259 | $plugin_i18n = new I18n( $this->get_plugin_name() );
260 |
261 | $this->loader->add_action( 'init', $plugin_i18n, 'load_plugin_textdomain' );
262 | }
263 |
264 | /**
265 | * Define the options for this plugin.
266 | *
267 | * @since 4.0.0
268 | * @access private
269 | */
270 | private function set_options(): void {
271 |
272 | $output_buffer = (bool) ini_get( 'output_buffering' );
273 | $masking_default = ! $output_buffer;
274 |
275 | $encryption = false;
276 | $encryption_key = false;
277 |
278 | if ( extension_loaded( 'openssl' ) ) {
279 | $encryption = 'openssl';
280 | $encryption_key = openssl_random_pseudo_bytes( 32, $strong_result );
281 |
282 | if ( false === $encryption_key || false === $strong_result ) {
283 | $encryption_key = md5( wp_rand() );
284 | }
285 | } elseif ( extension_loaded( 'mcrypt' ) ) {
286 | $encryption = 'mcrypt';
287 | $encryption_key = md5( wp_rand() );
288 | }
289 |
290 | // Default Options.
291 | $options = array(
292 | 'masking_type' => '302',
293 | 'redirect_time' => 3,
294 | 'mask_links' => $output_buffer ? 'all' : 'specific',
295 | 'mask_posts_pages' => $masking_default,
296 | 'mask_comments' => $masking_default,
297 | 'mask_comment_author' => $masking_default,
298 | 'mask_rss' => $masking_default,
299 | 'mask_rss_comments' => $masking_default,
300 | 'nofollow' => true,
301 | 'target_blank' => true,
302 | 'noindex_tag' => false,
303 | 'noindex_comment' => false,
304 | 'seo_hide' => false,
305 | 'seo_hide_mode' => 'specific',
306 | 'seo_hide_include_list' => '',
307 | 'seo_hide_exclude_list' => '',
308 | 'link_structure' => 'default',
309 | 'separator' => 'goto',
310 | 'link_encoding' => 'none',
311 | 'encryption' => $encryption,
312 | 'encryption_key' => $encryption_key,
313 | 'link_shortening' => 'none',
314 | 'adfly_api_key' => 'a722c6594441a443bafa644a820a8d3f',
315 | 'adfly_user_id' => '17681319',
316 | 'adfly_advert_type' => 2,
317 | 'adfly_domain' => 'adf.ly',
318 | 'bitly_login' => 'steamerdev',
319 | 'bitly_api_key' => 'R_31d62b0aa55e4c0abe306693624ff73a',
320 | 'shortest_api_key' => '57bfc99a0c2ce713061730b696750659',
321 | 'yourls_domain' => '',
322 | 'yourls_signature' => '',
323 | 'logging' => true,
324 | 'log_duration' => 0,
325 | 'remove_all_links' => false,
326 | 'links_to_text' => false,
327 | 'debug_mode' => false,
328 | 'anonymize_links' => false,
329 | 'anonymous_link_provider' => 'https://href.li/?',
330 | 'bot_targeting' => 'all',
331 | 'bots_selector' => [],
332 | 'check_referrer' => true,
333 | 'inclusion_list' => '',
334 | 'exclusion_list' => '',
335 | 'skip_auth' => false,
336 | 'skip_follow' => false,
337 | 'redirect_page' => 0,
338 | 'redirect_message' => 'You will be redirected in 3 seconds. If your browser does not automatically redirect you, please click here.',
339 | 'output_buffer' => $output_buffer,
340 | );
341 |
342 | $this->options = $this->validate_options( $options );
343 | }
344 |
345 | /**
346 | * Validates the options for this plugin.
347 | *
348 | * @param array $options Options.
349 | *
350 | * @return object $options
351 | * @since 4.2.0
352 | * @access private
353 | */
354 | private function validate_options( $options ) {
355 |
356 | $output_buffer = $options['output_buffer'];
357 |
358 | $encryption = $options['encryption'];
359 | $encryption_key = $options['encryption_key'];
360 |
361 | foreach ( $options as $key => $value ) {
362 | $option = get_option( $this->options_prefix . $key );
363 |
364 | switch ( $key ) {
365 | case 'masking_type':
366 | case 'link_structure':
367 | case 'link_shortening':
368 | case 'anonymous_link_provider':
369 | case 'inclusion_list':
370 | case 'seo_hide_mode':
371 | case 'seo_hide_include_list':
372 | case 'seo_hide_exclude_list':
373 | case 'exclusion_list':
374 | case 'bot_targeting':
375 | case 'redirect_message':
376 | if ( false !== $option ) {
377 | $options[ $key ] = (string) $option;
378 | }
379 |
380 | continue 2;
381 | case 'adfly_api_key':
382 | case 'adfly_user_id':
383 | case 'adfly_domain':
384 | case 'adfly_advert_type':
385 | case 'bitly_login':
386 | case 'bitly_api_key':
387 | case 'shortest_api_key':
388 | case 'yourls_domain':
389 | case 'yourls_signature':
390 | if ( false !== $option && '' !== $option ) {
391 | $options[ $key ] = (string) $option;
392 | }
393 |
394 | continue 2;
395 | case 'mask_links':
396 | if ( false !== $option ) {
397 | $options[ $key ] = (string) $option;
398 | }
399 |
400 | if ( ! $output_buffer ) {
401 | $options[ $key ] = 'specific';
402 | }
403 |
404 | continue 2;
405 | case 'link_encoding':
406 | if ( false !== $option ) {
407 | $options[ $key ] = (string) $option;
408 | }
409 |
410 | if ( 'aes256' === $option && ! $encryption ) {
411 | $options[ $key ] = 'none';
412 | }
413 |
414 | continue 2;
415 | case 'separator':
416 | if ( '' !== $option && false !== $option ) {
417 | $options[ $key ] = (string) $option;
418 | } else {
419 | $options[ $key ] = 'goto';
420 | }
421 |
422 | continue 2;
423 | case 'encryption_key':
424 | if ( '' === $option || false === $option ) {
425 | if ( $encryption_key ) {
426 | // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
427 | $encryption_key = base64_encode( $encryption_key );
428 |
429 | update_option( $this->options_prefix . $key, $encryption_key );
430 | $options[ $key ] = $encryption_key;
431 | } else {
432 | $options[ $key ] = false;
433 | }
434 | } else {
435 | $options[ $key ] = (string) $option;
436 | }
437 |
438 | continue 2;
439 | case 'redirect_time':
440 | case 'redirect_page':
441 | case 'log_duration':
442 | if ( false !== $option ) {
443 | $options[ $key ] = (int) $option;
444 | }
445 |
446 | continue 2;
447 | case 'bots_selector':
448 | if ( false !== $option && '' !== $option ) {
449 | $options[ $key ] = (array) $option;
450 | }
451 |
452 | continue 2;
453 | default:
454 | if ( false !== $option ) {
455 | $options[ $key ] = 1 === ( (int) $option );
456 | }
457 | }
458 | }
459 |
460 | return (object) $options;
461 | }
462 |
463 | /**
464 | * Initiates the plugin.
465 | *
466 | * @since 4.0.0
467 | * @access private
468 | * @noinspection SqlResolve
469 | */
470 | private function initiate(): void {
471 |
472 | $this->admin = new Admin(
473 | $this->get_plugin_name(),
474 | $this->get_version(),
475 | $this->get_options(),
476 | $this->get_options_prefix()
477 | );
478 |
479 | $this->public = new Frontend(
480 | $this->get_plugin_name(),
481 | $this->get_version(),
482 | $this->get_options()
483 | );
484 |
485 | if ( $this->options->skip_auth ) {
486 | $this->public->debug_info( 'Masking is enabled only for non logged in users' );
487 |
488 | // TODO: Look to improve this; without including pluggable.php.
489 | if ( ! function_exists( 'is_user_logged_in' ) ) {
490 | $this->public->debug_info( '\'is_user_logged_in\' function not found! Trying to include its file' );
491 |
492 | require_once ABSPATH . 'wp-includes/pluggable.php';
493 | }
494 | }
495 |
496 | if ( $this->options->logging && 0 !== $this->options->log_duration ) {
497 |
498 | global $wpdb;
499 |
500 | $table_name = $wpdb->prefix . 'external_links_logs';
501 |
502 | $current_time = current_time( 'mysql' );
503 |
504 | $last_cleared = get_option( $this->options_prefix . 'last_cleared_logs' );
505 |
506 | // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
507 | if ( ! $last_cleared || $last_cleared < current_time( 'timestamp' ) - 3600 * 24 ) {
508 | $sql = "DELETE FROM $table_name WHERE date < DATE_SUB('$current_time', INTERVAL %d DAY)";
509 |
510 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
511 | $wpdb->query( $wpdb->prepare( $sql, $this->options->log_duration ) );
512 |
513 | // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
514 | update_option( $this->options_prefix . 'last_cleared_logs', current_time( 'timestamp' ) );
515 | }
516 | }
517 | }
518 |
519 | /**
520 | * Register all the hooks related to the admin area functionality of the plugin.
521 | *
522 | * @since 4.0.0
523 | * @access private
524 | */
525 | private function define_admin_hooks(): void {
526 |
527 | $this->loader->add_action( 'admin_enqueue_scripts', $this->admin, 'enqueue_styles' );
528 | $this->loader->add_action( 'admin_enqueue_scripts', $this->admin, 'enqueue_scripts' );
529 |
530 | $this->loader->add_action( 'admin_menu', $this->admin, 'add_admin_pages' );
531 | $this->loader->add_action( 'admin_init', $this->admin, 'register_setting' );
532 |
533 | $this->loader->add_filter( 'install_plugins_nonmenu_tabs', $this->admin, 'install_plugins_nonmenu_tabs' );
534 | $this->loader->add_filter( 'install_plugins_table_api_args_' . MIHDAN_NO_EXTERNAL_LINKS_SLUG, $this->admin, 'install_plugins_table_api_args' );
535 |
536 | $this->loader->add_filter( 'set-screen-option', $this->admin, 'mask_page_set_screen_options', null, 3 );
537 |
538 | $hook_name = sprintf( 'load-no-external-links_page_%s-masks', $this->get_plugin_name() );
539 |
540 | $this->loader->add_action( $hook_name, $this->admin, 'mask_page_screen_options' );
541 |
542 | $this->loader->add_filter( 'set-screen-option', $this->admin, 'log_page_set_screen_options', null, 3 );
543 |
544 | $hook_name = sprintf( 'load-no-external-links_page_%s-logs', $this->get_plugin_name() );
545 |
546 | $this->loader->add_action( $hook_name, $this->admin, 'log_page_screen_options' );
547 |
548 | $this->loader->add_action( 'add_meta_boxes', $this->admin, 'add_custom_meta_box' );
549 | $this->loader->add_action( 'save_post', $this->admin, 'save_custom_meta_box' );
550 |
551 | $this->loader->add_action( 'init', $this->admin, 'site_health' );
552 | $this->loader->add_filter( 'plugin_action_links', $this->admin, 'add_settings_link', 10, 2 );
553 | }
554 |
555 | /**
556 | * Register all the hooks related to the public-facing functionality of the plugin.
557 | *
558 | * @since 4.0.0
559 | * @access private
560 | */
561 | private function define_public_hooks(): void {
562 |
563 | $this->loader->add_filter( 'template_redirect', $this->public, 'check_redirect', 1 );
564 |
565 | if ( $this->options->skip_auth && is_user_logged_in() ) {
566 | $this->public->debug_info( "User is authorised, we're not doing anything" );
567 | } else {
568 | if ( 'all' === $this->options->mask_links ) {
569 | $this->public->debug_info( 'Setting fullmask filters' );
570 | $this->loader->add_filter( 'wp', $this->public, 'fullpage_filter', 99 );
571 | } else {
572 | $this->public->debug_info( 'Setting per element filters' );
573 |
574 | if ( $this->options->mask_posts_pages ) {
575 | $this->loader->add_filter( 'the_content', $this->public, 'check_post', 99 );
576 | $this->loader->add_filter( 'the_excerpt', $this->public, 'check_post', 99 );
577 | }
578 |
579 | if ( $this->options->mask_comments ) {
580 | $this->loader->add_filter( 'comment_text', $this->public, 'filter', 99 );
581 | $this->loader->add_filter( 'comment_url', $this->public, 'filter', 99 );
582 | }
583 |
584 | if ( $this->options->mask_comment_author ) {
585 | $this->loader->add_filter( 'get_comment_author_url_link', $this->public, 'filter', 99 );
586 | $this->loader->add_filter( 'get_comment_author_link', $this->public, 'filter', 99 );
587 | $this->loader->add_filter( 'get_comment_author_url', $this->public, 'filter', 99 );
588 | }
589 | }
590 |
591 | if ( $this->options->mask_rss ) {
592 | $this->loader->add_filter( 'the_content_feed', $this->public, 'filter', 99 );
593 | $this->loader->add_filter( 'the_content_rss', $this->public, 'filter', 99 );
594 | $this->loader->add_filter( 'the_excerpt_rss', $this->public, 'filter', 99 );
595 | }
596 |
597 | if ( $this->options->mask_rss_comments ) {
598 | $this->loader->add_filter( 'comment_text_rss', $this->public, 'filter', 99 );
599 | }
600 | }
601 |
602 | if ( $this->options->debug_mode ) {
603 | $this->loader->add_action( 'wp_footer', $this->public, 'output_debug', 99 );
604 | }
605 | }
606 |
607 | /**
608 | * Run the loader to execute all the hooks with WordPress.
609 | *
610 | * @since 4.0.0
611 | */
612 | public function run(): void {
613 | $this->loader->run();
614 | }
615 |
616 | /**
617 | * The name of the plugin used to uniquely identify it within the context of
618 | * WordPress and to define internationalization functionality.
619 | *
620 | * @return string The name of the plugin.
621 | * @since 4.0.0
622 | */
623 | public function get_plugin_name(): string {
624 | return $this->plugin_name;
625 | }
626 |
627 | /**
628 | * The reference to the class that orchestrates the hooks with the plugin.
629 | *
630 | * @since 4.0.0
631 | * @return Loader Orchestrates the hooks of the plugin.
632 | */
633 | public function get_loader(): Loader {
634 | return $this->loader;
635 | }
636 |
637 | /**
638 | * Retrieve the version number of the plugin.
639 | *
640 | * @return string The version number of the plugin.
641 | * @since 4.0.0
642 | */
643 | public function get_version(): string {
644 | return $this->version;
645 | }
646 |
647 | /**
648 | * Retrieve the option prefix for the plugin.
649 | *
650 | * @return string The option prefix for the plugin.
651 | * @since 4.0.0
652 | */
653 | public function get_options_prefix(): string {
654 | return $this->options_prefix;
655 | }
656 |
657 | /**
658 | * Retrieve the list of options for the plugin.
659 | *
660 | * @return object The list of options for the plugin.
661 | * @since 4.0.0
662 | */
663 | public function get_options() {
664 | return $this->options;
665 | }
666 | }
667 |
--------------------------------------------------------------------------------
/public/Frontend.php:
--------------------------------------------------------------------------------
1 | plugin_name = $plugin_name;
92 | $this->options = $options;
93 |
94 | $this->setup_hooks();
95 | $this->initiate();
96 | }
97 |
98 | /**
99 | * Setup hooks.
100 | *
101 | * @return void
102 | */
103 | public function setup_hooks(): void {
104 | add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
105 | }
106 |
107 | /**
108 | * Start full page filtering.
109 | *
110 | * @since 4.2.0
111 | */
112 | public function fullpage_filter(): void {
113 |
114 | if ( defined( 'DOING_CRON' ) ) {
115 | // Do not try to use output buffering on cron.
116 | return;
117 | }
118 |
119 | ob_start( [ $this, 'ob_filter' ] );
120 | }
121 |
122 | /**
123 | * Output buffering function.
124 | *
125 | * @since 4.2.1
126 | *
127 | * @param string $content Content.
128 | *
129 | * @return string
130 | */
131 | public function ob_filter( $content ): string {
132 |
133 | global $post;
134 |
135 | if ( $content ) {
136 | $content = (string) preg_replace( '/(]*?>)/', '$1' . $this->data->buffer, $content );
137 |
138 | if (
139 | is_object( $post ) &&
140 | function_exists( 'is_feed' ) &&
141 | get_post_meta( $post->ID, 'mask_links', true ) !== 'disabled' &&
142 | ! is_feed()
143 | ) {
144 | // Excludes custom redirect page.
145 | if ( $this->options->redirect_page > 0 && $post->ID === (int) $this->options->redirect_page ) {
146 | return $content;
147 | } else {
148 | $content = $this->filter( $content );
149 | }
150 | }
151 | }
152 |
153 | return $content;
154 | }
155 |
156 | /**
157 | * Check if post/page should be masked.
158 | *
159 | * @since 4.0.0
160 | *
161 | * @param string $content Content.
162 | *
163 | * @return string mixed
164 | */
165 | public function check_post( $content ): string {
166 |
167 | global $post;
168 |
169 | if ( ! $post instanceof WP_Post ) {
170 | return $content;
171 | }
172 |
173 | $this->debug_info( 'Checking post for meta.' );
174 |
175 | $content = $this->data->before . $content . $this->data->after;
176 |
177 | if ( 'disabled' === get_post_meta( $post->ID, 'mask_links', true ) ) {
178 | $this->debug_info( 'Meta nomask. No masking will be applied' );
179 |
180 | return $content;
181 | }
182 |
183 | if ( $this->options->redirect_page > 0 && $post->ID === (int) $this->options->redirect_page ) {
184 | $this->debug_info( 'Missing custom redirect page' );
185 |
186 | return $content;
187 | }
188 |
189 | $this->debug_info( 'Filter will be applied' );
190 |
191 | return $this->filter( $content );
192 | }
193 |
194 | /**
195 | * Filters content to find links.
196 | *
197 | * @since 4.0.0
198 | *
199 | * @param string $content Content.
200 | *
201 | * @return string
202 | */
203 | public function filter( $content ): string {
204 |
205 | if ( function_exists( 'is_admin' ) && is_admin() ) {
206 | return $content;
207 | }
208 |
209 | $this->debug_info( "Processing text: \n" . str_replace( '-->', '-->', $content ) );
210 |
211 | if (
212 | function_exists( 'is_feed' ) &&
213 | ! $this->options->mask_rss && ! $this->options->mask_rss_comments &&
214 | is_feed()
215 | ) {
216 | $this->debug_info( 'It is feed, no processing' );
217 |
218 | return $content;
219 | }
220 |
221 | $pattern = '/(.*?)<\/a>/si';
222 |
223 | $content = preg_replace_callback( $pattern, [ $this, 'parser' ], $content, - 1, $count );
224 |
225 | $this->debug_info( $count . " replacements done.\nFilter returned: \n" . str_replace( '-->', '-->', $content ) );
226 |
227 | return $content;
228 | }
229 |
230 | /**
231 | * Determines whether to mask a link or not.
232 | *
233 | * @since 4.0.0
234 | *
235 | * @param array $matches Matches.
236 | *
237 | * @return string
238 | * @noinspection MultiAssignmentUsageInspection
239 | */
240 | public function parser( $matches ): string {
241 |
242 | $anchor = $matches[0];
243 | $href = $matches[2];
244 |
245 | // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export
246 | $this->debug_info( 'Parser called. Parsing argument {' . var_export( $matches, 1 ) . "}\nAgainst link {" . $href . "}\n " );
247 |
248 | if ( preg_match( '/ rel=[\"\']exclude[\"\']/i', $anchor, $match ) ) {
249 | $this->exclusion_list[] = $href;
250 |
251 | return str_replace( $match[0], '', $anchor );
252 | }
253 |
254 | if ( '' !== $this->options->inclusion_list ) {
255 | $inclusion_list = explode( "\r\n", $this->options->inclusion_list );
256 |
257 | foreach ( $inclusion_list as $inclusion_url ) {
258 | if ( strpos( $href, $inclusion_url ) === 0 ) {
259 | return $this->mask_link( $matches );
260 | }
261 | }
262 | } else {
263 | $this->debug_info( 'Checking link "' . $href . '" VS exclusion list {' . var_export( $this->exclusion_list, 1 ) . '}' );
264 |
265 | foreach ( $this->exclusion_list as $exclusion_url ) {
266 | if ( stripos( $href, $exclusion_url ) === 0 ) {
267 | $this->debug_info( 'In exclusion list (' . $exclusion_url . '), not masking...' );
268 |
269 | return $matches[0];
270 | }
271 | }
272 |
273 | $this->debug_info( 'Not in exclusion list, masking...' );
274 |
275 | return $this->mask_link( $matches );
276 | }
277 |
278 | // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_var_export
279 |
280 | return $matches[0];
281 | }
282 |
283 | /**
284 | * Performs link masking functionality.
285 | *
286 | * @since 4.2.0
287 | *
288 | * @param array $matches Matches.
289 | *
290 | * @return string $anchor
291 | * @noinspection MultiAssignmentUsageInspection
292 | */
293 | public function mask_link( $matches ): string {
294 |
295 | global $wp_rewrite;
296 |
297 | $anchor = $matches[0];
298 | $anchor_text = $matches[4];
299 | $attributes = trim( $matches[1] ) . ' ' . trim( $matches[3] );
300 | $url = $matches[2];
301 |
302 | if ( 'all' !== $this->options->bot_targeting && ! in_array( $this->data->user_agent_name, $this->options->bots_selector, true ) ) {
303 | $this->debug_info( 'User agent targeting does not match, not masking it.' );
304 |
305 | return $anchor;
306 | }
307 |
308 | if ( $this->options->skip_follow ) {
309 | if ( preg_match( '/rel=[\"\'].*?(?debug_info( 'This link has a follow attribute not masking it.' );
311 |
312 | return $anchor;
313 | }
314 |
315 | $this->debug_info( 'it does not have rel follow, masking it.' );
316 | }
317 |
318 | $separator = $wp_rewrite->using_permalinks() ? $this->options->separator . '/' : '?' . $this->options->separator . '=';
319 | $blank = $this->options->target_blank ? ' target="_blank"' : '';
320 | $nofollow = $this->options->nofollow ? ' rel="nofollow"' : '';
321 |
322 | if ( $this->options->seo_hide ) {
323 | // Get classes.
324 | $classes = 'waslinkname';
325 |
326 | preg_match( '/class="([^"]+)"/si', $attributes, $maybe_classes );
327 |
328 | if ( ! empty( $maybe_classes[1] ) ) {
329 | $classes .= ' ' . $maybe_classes[1];
330 | }
331 |
332 | // Получает доменное имя из URL.
333 | $current_domain = $this->get_domain_from_url( $url );
334 |
335 | // Список включений.
336 | $seo_hide_include_list = $this->textarea_to_array( $this->options->seo_hide_include_list );
337 |
338 | // Список исключений.
339 | $seo_hide_exclude_list = $this->textarea_to_array( $this->options->seo_hide_exclude_list );
340 |
341 | // Маскировать только указанные ссылки.
342 | if ( 'specific' === $this->options->seo_hide_mode ) {
343 | if ( in_array( $current_domain, $seo_hide_include_list, true ) ) {
344 | return sprintf(
345 | '%s',
346 | esc_attr( $classes ),
347 | esc_attr( base64_encode( $url ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
348 | str_replace( 'target', 'data-target', $blank ),
349 | $anchor_text
350 | );
351 | }
352 | }
353 |
354 | // Маскировать все ссылки, кроме указанных.
355 | if ( 'all' === $this->options->seo_hide_mode ) {
356 | if ( ! in_array( $current_domain, $seo_hide_exclude_list, true ) ) {
357 | return sprintf(
358 | '%s',
359 | esc_attr( $classes ),
360 | esc_attr( base64_encode( $url ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
361 | str_replace( 'target', 'data-target', $blank ),
362 | $anchor_text
363 | );
364 | }
365 | }
366 | }
367 |
368 | if ( 'none' !== $this->options->link_shortening ) {
369 | $url = $this->shorten_link( $url );
370 |
371 | $this->exclusion_list[] = $url;
372 |
373 | return '' . $anchor_text . '';
374 | }
375 |
376 | if ( 'no' !== $this->options->masking_type ) {
377 | $url = $this->encode_link( $url );
378 |
379 | if ( ! $wp_rewrite->using_permalinks() ) {
380 | $url = rawurlencode( $url );
381 | }
382 |
383 | $url = trim( $this->data->site, '/' ) . '/' . $separator . $url;
384 | }
385 |
386 | if ( $this->options->remove_all_links ) {
387 | return '' . $anchor_text . '';
388 | }
389 |
390 | if ( $this->options->links_to_text ) {
391 | return '' . $anchor_text . ' ^(' . $url . ')';
392 | }
393 |
394 | $anchor = '' . $anchor_text . '';
395 |
396 | if ( $this->options->noindex_tag ) {
397 | $anchor = '';
398 | }
399 |
400 | if ( $this->options->noindex_comment ) {
401 | $anchor = '' . $anchor . '';
402 | }
403 |
404 | return $anchor;
405 | }
406 |
407 | /**
408 | * Convert textarea value to PHP array.
409 | *
410 | * @param string $field Textarea value.
411 | *
412 | * @return array
413 | */
414 | private function textarea_to_array( $field ): array {
415 | return array_map(
416 | 'trim',
417 | (array) explode( PHP_EOL, trim( $field ) )
418 | );
419 | }
420 |
421 | /**
422 | * Get domain name from absolute URL.
423 | *
424 | * @param string $url Given URL.
425 | *
426 | * @return string
427 | */
428 | private function get_domain_from_url( string $url ): string {
429 | return (string) wp_parse_url( $url, PHP_URL_HOST );
430 | }
431 |
432 | /**
433 | * Checks if current page is a redirect page.
434 | *
435 | * @since 4.0.0
436 | */
437 | public function check_redirect(): void {
438 | $goto = '';
439 | $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
440 | $p = strpos( $uri, '/' . $this->options->separator . '/' );
441 |
442 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
443 | $sep = isset( $_REQUEST[ $this->options->separator ] ) ?
444 | sanitize_key( wp_unslash( $_REQUEST[ $this->options->separator ] ) ) :
445 | '';
446 | // phpcs:enable WordPress.Security.NonceVerification.Recommended
447 |
448 | if ( $sep ) {
449 | $goto = $sep;
450 | } elseif ( false !== $p ) {
451 | $goto = substr( $uri, $p + strlen( $this->options->separator ) + 2 );
452 | }
453 |
454 | $goto = wp_strip_all_tags( $goto );
455 |
456 | if ( ! empty( $goto ) ) {
457 | $this->redirect( $goto );
458 | }
459 | }
460 |
461 | /**
462 | * Initiates the redirect.
463 | *
464 | * @since 4.0.0
465 | *
466 | * @param string $url Url.
467 | */
468 | public function redirect( $url ): void {
469 |
470 | global $wp_query, $wp_rewrite, $hyper_cache_stop;
471 |
472 | // Disable Hyper Cache plugin (http://www.satollo.net/plugins/hyper-cache) from caching this page.
473 | $hyper_cache_stop = true;
474 |
475 | // Disable WP Super Cache, WP Rocket caching.
476 | if ( ! defined( 'DONOTCACHEPAGE' ) ) {
477 | define( 'DONOTCACHEPAGE', 1 );
478 | }
479 |
480 | // Disable WP Rocket optimize.
481 | if ( ! defined( 'DONOTROCKETOPTIMIZE' ) ) {
482 | define( 'DONOTROCKETOPTIMIZE', true );
483 | }
484 |
485 | // Prevent 404.
486 | if ( $wp_query->is_404 ) {
487 | $wp_query->is_404 = false;
488 | header( 'HTTP/1.1 200 OK', true );
489 | }
490 |
491 | // Checking for spammer attack, redirect should happen from your own website.
492 | if ( $this->options->check_referrer ) {
493 | $referer = wp_get_referer();
494 |
495 | if ( $referer && stripos( $referer, $this->data->site ) ) {
496 | $this->show_referrer_warning();
497 | }
498 | }
499 |
500 | $url = $this->decode_link( $url );
501 |
502 | $this->add_log( $url );
503 |
504 | if ( ! $wp_rewrite->using_permalinks() ) {
505 | $url = urldecode( $url );
506 | }
507 |
508 | // Restore & and & to &.
509 | $url = html_entity_decode( $url, ENT_HTML5 | ENT_QUOTES, get_option( 'blog_charset' ) );
510 |
511 | if ( $this->options->anonymize_links ) {
512 | $url = $this->options->anonymous_link_provider . $url;
513 | }
514 |
515 | $this->show_redirect_page( $url );
516 | }
517 |
518 | /**
519 | * Initiate required info for functions.
520 | *
521 | * @since 4.2.0
522 | * @noinspection SqlResolve
523 | * @noinspection HttpUrlsUsage
524 | */
525 | public function initiate(): void {
526 |
527 | global $wpdb;
528 |
529 | $this->data = new stdClass();
530 | $this->data->after = null;
531 | $this->data->before = null;
532 | $this->data->buffer = null;
533 | $this->data->client_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : null;
534 | $this->data->ip = isset( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : null;
535 | $this->data->referring_url = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : null;
536 | $this->data->site = get_option( 'home' ) ?: get_option( 'siteurl' );
537 | $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : null;
538 | $this->data->url = $this->data->site . $request_uri;
539 | $this->data->user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : null;
540 |
541 | if ( $this->data->user_agent ) {
542 | if (
543 | preg_match(
544 | '/(compatible;\sMSIE(?:[a-z\-]+)?\s(?:\d\.\d);\sAOL\s(?:\d\.\d);\sAOLBuild)/',
545 | $this->data->user_agent
546 | )
547 | ) {
548 | $this->data->user_agent_id = 2;
549 | $this->data->user_agent_name = 'aol';
550 | } elseif (
551 | preg_match(
552 | '/(compatible;\sBingbot(?:[a-z\-]+)?.*\/(?:\d\.\d);[\s\+]+http\:\/\/www\.bing\.com\/bingbot\.htm\))/',
553 | $this->data->user_agent
554 | )
555 | ) {
556 | $this->data->user_agent_id = 1;
557 | $this->data->user_agent_name = 'bingbot';
558 | } elseif (
559 | preg_match(
560 | '/(msnbot\/(?:\d\.\d)(?:[a-z]?)[\s\+]+\(\+http\:\/\/search\.msn\.com\/msnbot\.htm\))/',
561 | $this->data->user_agent
562 | )
563 | ) {
564 | $this->data->user_agent_id = 1;
565 | $this->data->user_agent_name = 'bingbot';
566 | } elseif (
567 | preg_match(
568 | '/(compatible;\sGooglebot(?:[a-z\-]+)?.*\/(?:\d\.\d);[\s\+]+http\:\/\/www\.google\.com\/bot\.html\))/',
569 | $this->data->user_agent
570 | )
571 | ) {
572 | $this->data->user_agent_id = 3;
573 | $this->data->user_agent_name = 'googlebot';
574 | } elseif (
575 | preg_match(
576 | '/(compatible;\sAsk Jeeves\/Teoma)/',
577 | $this->data->user_agent
578 | )
579 | ) {
580 | $this->data->user_agent_id = 4;
581 | $this->data->user_agent_name = 'ask';
582 | } elseif (
583 | preg_match(
584 | '/(compatible;\sYahoo!(?:[a-z\-]+)?.*;[\s\+]+http\:\/\/help\.yahoo\.com\/)/',
585 | $this->data->user_agent
586 | )
587 | ) {
588 | $this->data->user_agent_id = 5;
589 | $this->data->user_agent_name = 'yahoo';
590 | } elseif (
591 | preg_match(
592 | '/(compatible;\sBaiduspider(?:[a-z\-]+)?.*\/(?:\d\.\d);[\s\+]+http\:\/\/www\.baidu\.com\/search\/spider\.html\))/',
593 | $this->data->user_agent
594 | )
595 | ) {
596 | $this->data->user_agent_id = 6;
597 | $this->data->user_agent_name = 'baiduspider';
598 | } elseif (
599 | preg_match(
600 | '/(Baiduspider[\+]+\(\+http\:\/\/www\.baidu\.com\/search)/',
601 | $this->data->user_agent
602 | )
603 | ) {
604 | $this->data->user_agent_id = 6;
605 | $this->data->user_agent_name = 'baiduspider';
606 | } elseif (
607 | preg_match(
608 | '/(DuckDuckBot(?:[a-z\-]+)?.*\/(?:\d\.\d);[\s]+\(\+http\:\/\/duckduckgo\.com\/duckduckbot\.html\))/',
609 | $this->data->user_agent
610 | )
611 | ) {
612 | $this->data->user_agent_id = 8;
613 | $this->data->user_agent_name = 'duckduckbot';
614 | } elseif (
615 | preg_match(
616 | '/(compatible;\sYandexBot(?:[a-z\-]+)?.*\/(?:\d\.\d);[\s\+]+http\:\/\/yandex\.com\/bots\))/',
617 | $this->data->user_agent
618 | )
619 | ) {
620 | $this->data->user_agent_id = 10;
621 | $this->data->user_agent_name = 'yandexbot';
622 | }
623 | }
624 |
625 | $table_name = $wpdb->prefix . 'external_links_masks';
626 |
627 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
628 | $result = $wpdb->get_col(
629 | "SELECT mask FROM $table_name LIMIT 10000"
630 | );
631 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
632 |
633 | $site = str_replace( [ 'http://', 'https://' ], '', $this->data->site );
634 |
635 | $exclude_links = [
636 | 'http://' . $site,
637 | 'https://' . $site,
638 | 'javascript',
639 | 'magnet',
640 | 'mailto',
641 | 'skype',
642 | 'tel',
643 | '/',
644 | '#',
645 | 'https://wordpress.org/',
646 | 'https://codex.wordpress.org/',
647 | ];
648 |
649 | if ( '' !== $this->options->exclusion_list ) {
650 | $exclusion_list = explode( "\r\n", $this->options->exclusion_list );
651 |
652 | foreach ( $exclusion_list as $item ) {
653 | $exclude_links[] = $item;
654 | }
655 | }
656 |
657 | if ( is_array( $result ) && count( $result ) > 0 ) {
658 | $exclude_links = array_merge( $exclude_links, $result );
659 | }
660 |
661 | $this->exclusion_list = array_filter( $exclude_links );
662 | }
663 |
664 | /**
665 | * Encodes url.
666 | *
667 | * @param string $url Url.
668 | *
669 | * @return string
670 | * @noinspection PhpUndefinedClassInspection
671 | * @noinspection SqlResolve
672 | * @noinspection CryptographicallySecureAlgorithmsInspection
673 | * @noinspection EncryptionInitializationVectorRandomnessInspection
674 | *
675 | * @since 4.0.0
676 | */
677 | public function encode_link( string $url ): string {
678 |
679 | global $wpdb;
680 |
681 | // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
682 | // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
683 |
684 | switch ( $this->options->link_encoding ) {
685 | case 'aes256':
686 | $encryption_key = base64_decode( $this->options->encryption_key );
687 | $iv = '';
688 |
689 | if ( 'openssl' === $this->options->encryption ) {
690 | $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'AES-256-CBC' ), $strong_result );
691 |
692 | if ( false === $iv || false === $strong_result ) {
693 | $iv = md5( wp_rand() );
694 | }
695 |
696 | $url = openssl_encrypt( $url, 'AES-256-CBC', $encryption_key, 0, $iv );
697 | } elseif ( 'mcrypt' === $this->options->encryption ) {
698 | $iv = mcrypt_create_iv( mcrypt_get_iv_size( MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB ), MCRYPT_DEV_RANDOM );
699 |
700 | $url = trim( base64_encode( mcrypt_encrypt( MCRYPT_RIJNDAEL_256, $encryption_key, $url, MCRYPT_MODE_ECB, $iv ) ) );
701 | }
702 |
703 | $url .= ':' . base64_encode( $iv );
704 |
705 | break;
706 | case 'base64':
707 | $url = base64_encode( $url );
708 |
709 | break;
710 | case 'numbers':
711 | $table_name = $wpdb->prefix . 'external_links_masks';
712 | $sql = 'SELECT id FROM ' . $table_name . ' WHERE LOWER(url) = LOWER(%s) LIMIT 1';
713 |
714 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
715 | $result = $wpdb->get_var(
716 | // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
717 | $wpdb->prepare( $sql, $url )
718 | );
719 |
720 | // No table found.
721 | if ( is_null( $result ) && strpos( $wpdb->last_error, "doesn't exist" ) ) {
722 | $create = Database::migrate( 'external_links_masks' );
723 |
724 | if ( empty( $create ) ) {
725 | $this->debug_info(
726 | __( 'Unable to create "external_links_masks" table.', $this->plugin_name )
727 | );
728 | }
729 | } elseif ( is_null( $result ) ) {
730 | $this->debug_info(
731 | __( 'Failed SQL: ', $this->plugin_name ) . '
' .
732 | $sql . '
' .
733 | __( 'Error was:', $this->plugin_name ) . '
' .
734 | $wpdb->last_error
735 | );
736 | }
737 |
738 | if ( ! $result ) {
739 | $insert = $wpdb->insert(
740 | $wpdb->prefix . 'external_links_masks',
741 | [ 'url' => $url ]
742 | );
743 |
744 | if ( 0 === $insert ) {
745 | $this->debug_info(
746 | __( 'Failed SQL: ', $this->plugin_name ) . '
' .
747 | $sql . '
' .
748 | __( 'Error was:', $this->plugin_name ) . '
' .
749 | $wpdb->last_error
750 | );
751 | }
752 |
753 | $url = (string) $wpdb->insert_id;
754 | } else {
755 | $url = $result;
756 | }
757 |
758 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
759 |
760 | break;
761 | }
762 |
763 | // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
764 | // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
765 |
766 | return $url;
767 | }
768 |
769 | /**
770 | * Decodes encoded url.
771 | *
772 | * @param string $url Url.
773 | *
774 | * @return string $url
775 | * @noinspection SqlResolve
776 | * @noinspection CryptographicallySecureAlgorithmsInspection
777 | *
778 | * @since 4.0.0
779 | */
780 | public function decode_link( $url ): string {
781 |
782 | global $wpdb;
783 |
784 | // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
785 | // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
786 |
787 | switch ( $this->options->link_encoding ) {
788 | case 'aes256':
789 | $encryption_key = base64_decode( $this->options->encryption_key );
790 |
791 | [ $encrypted, $iv ] = explode( ':', $url );
792 |
793 | if ( 'openssl' === $this->options->encryption ) {
794 | $url = (string) openssl_decrypt( $encrypted, 'AES-256-CBC', $encryption_key, 0, base64_decode( $iv ) );
795 | } elseif ( 'mcrypt' === $this->options->encryption ) {
796 | $url = trim( mcrypt_decrypt( MCRYPT_RIJNDAEL_256, $encryption_key, base64_decode( $encrypted ), MCRYPT_MODE_ECB, base64_decode( $iv ) ) );
797 | }
798 |
799 | break;
800 | case 'base64':
801 | $url = (string) base64_decode( $url );
802 |
803 | break;
804 | case 'numbers':
805 | $table_name = $wpdb->prefix . 'external_links_masks';
806 | $sql = 'SELECT url FROM ' . $table_name . ' WHERE id = %s LIMIT 1';
807 |
808 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
809 | $url = (string) $wpdb->get_var( $wpdb->prepare( $sql, $url ) );
810 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
811 |
812 | break;
813 | }
814 |
815 | // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
816 | // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
817 |
818 | return $url;
819 | }
820 |
821 | /**
822 | * Shortens link.
823 | *
824 | * @since 4.2.0
825 | *
826 | * @param string $url Url.
827 | *
828 | * @return mixed
829 | * @noinspection SqlResolve
830 | * @noinspection HttpUrlsUsage
831 | * @throws JsonException JsonException.
832 | */
833 | public function shorten_link( $url ) {
834 |
835 | global $wpdb;
836 |
837 | $table_name = $wpdb->prefix . 'external_links_masks';
838 | $long_url = rawurlencode( $url );
839 |
840 | // Restore original URL.
841 | $url = html_entity_decode( $url, ENT_HTML5 | ENT_QUOTES, get_option( 'blog_charset' ) );
842 |
843 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
844 | switch ( $this->options->link_shortening ) {
845 | case 'adfly':
846 | $shortener = 'adfly';
847 |
848 | $sql = "SELECT mask FROM $table_name WHERE LOWER(url) = LOWER(%s) AND LOWER(short_url) = 'adfly' LIMIT 1";
849 | $result = $wpdb->get_var( $wpdb->prepare( $sql, $url ) );
850 |
851 | if ( $result ) {
852 | return $result;
853 | }
854 |
855 | $api_url = 'https://api.adf.ly/v1/shorten';
856 | $query = [
857 | 'timeout' => 2,
858 | 'body' => [
859 | 'domain' => $this->options->adfly_domain,
860 | 'advert_type' => $this->options->adfly_advert_type,
861 | 'url' => urldecode( $long_url ),
862 | '_api_key' => $this->options->adfly_api_key,
863 | '_user_id' => $this->options->adfly_user_id,
864 | ],
865 | ];
866 | $response = wp_remote_post( $api_url, $query );
867 | $json = wp_remote_retrieve_body( $response );
868 |
869 | if ( $json ) {
870 | $json = json_decode( $json, false, 512, JSON_THROW_ON_ERROR );
871 | $short_url = $json->data[0]->short_url;
872 | }
873 |
874 | break;
875 | case 'bitly':
876 | $shortener = 'bitly';
877 |
878 | $sql = "SELECT mask FROM $table_name WHERE LOWER(url) = LOWER(%s) AND LOWER(short_url) = 'bitly' LIMIT 1";
879 | $result = $wpdb->get_var( $wpdb->prepare( $sql, $url ) );
880 |
881 | if ( $result ) {
882 | return $result;
883 | }
884 |
885 | $api_url = 'https://api-ssl.bitly.com/v3/shorten?login=' . $this->options->bitly_login . '&apiKey=' . $this->options->bitly_api_key . '&longUrl=' . $long_url;
886 | $response = wp_remote_get( $api_url, [ 'timeout' => 2 ] );
887 |
888 | if ( $response['body'] ) {
889 | $data = json_decode( $response['body'], false, 512, JSON_THROW_ON_ERROR );
890 |
891 | $short_url = $data->data->url;
892 | }
893 |
894 | break;
895 | case 'shortest':
896 | $shortener = 'shortest';
897 |
898 | $sql = "SELECT mask FROM $table_name WHERE LOWER(url) = LOWER(%s) AND LOWER(short_url) = 'shortest' LIMIT 1";
899 | $result = $wpdb->get_var( $wpdb->prepare( $sql, $url ) );
900 |
901 | if ( $result ) {
902 | return $result;
903 | }
904 |
905 | $api_url = 'https://api.shorte.st/s/' . $this->options->shortest_api_key . '/' . $long_url;
906 | $response = wp_remote_get( $api_url, [ 'timeout' => 2 ] );
907 |
908 | if ( $response['body'] ) {
909 | $data = json_decode( $response['body'], false, 512, JSON_THROW_ON_ERROR );
910 |
911 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
912 | $short_url = $data->shortenedUrl;
913 | }
914 |
915 | break;
916 | case 'yourls':
917 | $shortener = 'yourls';
918 |
919 | $sql = "SELECT mask FROM $table_name WHERE LOWER(url) = LOWER(%s) AND LOWER(short_url) = 'yourls' LIMIT 1";
920 | $result = $wpdb->get_var( $wpdb->prepare( $sql, $url ) );
921 |
922 | if ( $result ) {
923 | return $result;
924 | }
925 |
926 | $host = 'https://' . $this->options->yourls_domain . '/yourls-api.php';
927 | $query = [
928 | 'action' => 'shorturl',
929 | 'format' => 'json',
930 | 'signature' => $this->options->yourls_signature,
931 | 'url' => $long_url,
932 | ];
933 | $query = http_build_query( $query );
934 |
935 | $response = wp_remote_get(
936 | $host . '?' . $query,
937 | [
938 | 'timeout' => 2,
939 | ]
940 | );
941 |
942 | $json = wp_remote_retrieve_body( $response );
943 |
944 | if ( $json ) {
945 | $json = json_decode( $json, false, 512, JSON_THROW_ON_ERROR );
946 | $short_url = $json->shorturl;
947 | } else {
948 | $short_url = $long_url;
949 | }
950 |
951 | break;
952 | }
953 |
954 | if ( isset( $short_url, $shortener ) ) {
955 | $wpdb->insert(
956 | $wpdb->prefix . 'external_links_masks',
957 | [
958 | 'url' => $url,
959 | 'mask' => $short_url,
960 | 'short_url' => $shortener,
961 | ]
962 | );
963 |
964 | return $short_url;
965 | }
966 |
967 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
968 |
969 | return $url;
970 | }
971 |
972 | /**
973 | * Adds log record to database.
974 | *
975 | * @since 4.0.0
976 | *
977 | * @param string $url Url.
978 | *
979 | * @noinspection PhpUndefinedClassInspection*/
980 | public function add_log( $url ): void {
981 |
982 | global $wpdb;
983 |
984 | if ( ! $this->options->logging ) {
985 | return;
986 | }
987 |
988 | // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
989 | $insert = $wpdb->insert(
990 | $wpdb->prefix . 'external_links_logs',
991 | [
992 | 'url' => $url,
993 | 'user_agent' => $this->data->user_agent,
994 | 'referring_url' => $this->data->referring_url,
995 | 'ip_address' => $this->data->client_ip,
996 | 'date' => current_time( 'mysql' ),
997 | ]
998 | );
999 |
1000 | if ( false !== $insert ) {
1001 | return; // All OK.
1002 | }
1003 |
1004 | // Error - stats record could not be created.
1005 | $this->debug_info(
1006 | __( 'Failed SQL: ', $this->plugin_name ) . '
' .
1007 | $wpdb->last_query . '
' .
1008 | __( 'Error was:', $this->plugin_name ) . '
' .
1009 | $wpdb->last_error
1010 | );
1011 |
1012 | $create = Database::migrate( 'external_links_logs' );
1013 |
1014 | if ( empty( $create ) ) {
1015 | $this->debug_info(
1016 | __( 'Unable to create "external_links_logs" table.', $this->plugin_name )
1017 | );
1018 | } else {
1019 | $wpdb->insert(
1020 | $wpdb->prefix . 'external_links_logs',
1021 | [
1022 | 'url' => $url,
1023 | 'date' => current_time( 'mysql' ),
1024 | ]
1025 | );
1026 | }
1027 | // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
1028 | }
1029 |
1030 | /**
1031 | * Renders the referrer warning page.
1032 | */
1033 | public function show_referrer_warning(): void {
1034 | header( 'Content-type: text/html; charset="utf-8"', true );
1035 | header( 'Refresh: ' . $this->options->redirect_time . '; url=' . get_home_url() );
1036 |
1037 | include_once 'partials/referrer-warning.php';
1038 | }
1039 |
1040 | /**
1041 | * Renders the redirect page.
1042 | *
1043 | * @since 4.0.0
1044 | *
1045 | * @param string $url Url.
1046 | */
1047 | public function show_redirect_page( $url ): void {
1048 | $url = trim( $url );
1049 | $redirect_time = absint( $this->options->redirect_time );
1050 | $masking_type = trim( $this->options->masking_type );
1051 |
1052 | header( 'Content-type: text/html; charset="utf-8"', true );
1053 |
1054 | // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
1055 | if ( $url ) {
1056 | if ( '301' === $masking_type ) {
1057 | @header( 'Location: ' . $url, true, 301 );
1058 | die;
1059 | }
1060 |
1061 | if ( '302' === $masking_type ) {
1062 | @header( 'Location: ' . $url, true, 302 );
1063 | die;
1064 | }
1065 |
1066 | if ( '307' === $masking_type ) {
1067 | @header( 'Location: ' . $url, true, 307 );
1068 | die;
1069 | }
1070 |
1071 | if ( 'javascript' === $masking_type ) {
1072 | header( 'Refresh: ' . $redirect_time . '; url=' . $url );
1073 | }
1074 |
1075 | if ( $this->options->redirect_page > 0 ) {
1076 | // Disable page indexing.
1077 | header( 'X-Robots-Tag: noindex, nofollow', true );
1078 |
1079 | $page_content = wp_remote_get( get_permalink( $this->options->redirect_page ) );
1080 |
1081 | if ( $page_content ) {
1082 | echo preg_replace( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1083 | '#(https?://)?%linkurl%#i',
1084 | esc_url( $url ),
1085 | wp_remote_retrieve_body( $page_content )
1086 | );
1087 | die;
1088 | }
1089 | } else {
1090 | include_once 'partials/redirect.php';
1091 | }
1092 | }
1093 |
1094 | // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
1095 | }
1096 |
1097 | /**
1098 | * Outputs the debug log.
1099 | *
1100 | * TODO: Look to move to admin class
1101 | *
1102 | * @since 4.0.0
1103 | */
1104 | public function output_debug(): void {
1105 |
1106 | echo "\n";
1107 | }
1108 |
1109 | /**
1110 | * Adds data to the debug log.
1111 | *
1112 | * TODO: Look to move to admin class
1113 | *
1114 | * @since 4.0.0
1115 | *
1116 | * @param string $info Info.
1117 | * @param int $ret Whether return the logged info or empty string.
1118 | *
1119 | * @return string
1120 | */
1121 | public function debug_info( $info, $ret = 0 ): string {
1122 |
1123 | if ( $this->options->debug_mode ) {
1124 | $t = "\n";
1125 | $this->debug_log[] = $info;
1126 |
1127 | if ( $ret ) {
1128 | return $t;
1129 | }
1130 | }
1131 |
1132 | return '';
1133 | }
1134 |
1135 | /**
1136 | * Enqueue scripts.
1137 | *
1138 | * @return void
1139 | */
1140 | public function enqueue_scripts(): void {
1141 | if ( $this->options->seo_hide ) {
1142 | wp_enqueue_style(
1143 | MIHDAN_NO_EXTERNAL_LINKS_SLUG . '-seo-hide',
1144 | MIHDAN_NO_EXTERNAL_LINKS_URL . '/public/css/seo-hide.css',
1145 | [],
1146 | MIHDAN_NO_EXTERNAL_LINKS_VERSION
1147 | );
1148 | wp_enqueue_script(
1149 | MIHDAN_NO_EXTERNAL_LINKS_SLUG . '-seo-hide',
1150 | MIHDAN_NO_EXTERNAL_LINKS_URL . '/public/js/seo-hide.js',
1151 | [],
1152 | filemtime( MIHDAN_NO_EXTERNAL_LINKS_DIR . '/public/js/seo-hide.js' ),
1153 | true
1154 | );
1155 | }
1156 | }
1157 | }
1158 |
--------------------------------------------------------------------------------