├── 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 |
14 |

15 | 16 |
17 | logs_table->prepare_items(); 19 | $this->logs_table->display(); 20 | ?> 21 |
22 |
23 | -------------------------------------------------------------------------------- /admin/partials/masks.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |

15 | 16 |
17 | masks_table->prepare_items(); 19 | $this->masks_table->display(); 20 | ?> 21 |
22 |
23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [{.jshintrc,*.json,*.yml,*.scss}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [phpcs.xml] 25 | indent_style = space 26 | indent_size = 4 27 | insert_final_newline = false 28 | -------------------------------------------------------------------------------- /admin/css/mihdan-noexternallinks-admin.min.css: -------------------------------------------------------------------------------- 1 | input[name^=mihdan_noexternallinks_]:read-only,textarea[name^=mihdan_noexternallinks_]:read-only{background:rgba(255,255,255,.5);border-color:rgba(222,222,222,.75);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.04);box-shadow:inset 0 1px 2px rgba(0,0,0,.04);color:rgba(51,51,51,.5)}div.list-tree,div.list-tree li,div.list-tree ul{position:relative}div.list-tree>ul{margin:0;padding-left:20px}div.list-tree li::after,div.list-tree li::before{content:"";left:-12px;position:absolute}div.list-tree li::before{border-top:1px solid #b4b9be;height:0;top:12px;width:8px}div.list-tree li::after{border-left:1px solid #b4b9be;height:120%;top:-2px;width:0}div.list-tree ul>li:last-child::after{height:14px}.mihdan_noexternallinks_hidden{display:none!important} -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We take the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations. 4 | 5 | If you believe you have found a security vulnerability in the repository, please report it to us as described below. 6 | 7 | ## How can I report security bugs? 8 | 9 | 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) 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | ## Preferred Languages 14 | 15 | We prefer all communications to be in English. 16 | 17 | ## Supported Versions 18 | 19 | Generally, *only the latest version of the plugin has continued support*. If a critical vulnerability is found in the current version of the plugin, we may opt to backport any patches to previous versions. 20 | -------------------------------------------------------------------------------- /languages/mihdan-noexternallinks.pot: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: PACKAGE VERSION\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2018-01-16 13:55+0000\n" 7 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 8 | "Last-Translator: FULL NAME \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 | <?php esc_html_e( 'Redirecting...', $this->plugin_name ); ?> 23 | 24 | 25 |
26 | plugin_name 30 | ); 31 | echo ( 32 | esc_html( $message ) . 33 | '' . 34 | esc_html__( 'safe web site.', $this->plugin_name ) . 35 | '' 36 | ); 37 | ?> 38 |
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 |
57 | plugin_name . '-settings-links' ); 60 | do_settings_sections( $this->plugin_name . '-settings-links' ); 61 | submit_button(); 62 | } elseif ( 'include_exclude' === $active_tab ) { 63 | settings_fields( $this->plugin_name . '-settings-include-exclude' ); 64 | do_settings_sections( $this->plugin_name . '-settings-include-exclude' ); 65 | submit_button(); 66 | } elseif ( 'seo_hide' === $active_tab ) { 67 | settings_fields( $this->plugin_name . '-settings-seo-hide' ); 68 | do_settings_sections( $this->plugin_name . '-settings-seo-hide' ); 69 | submit_button(); 70 | } elseif ( 'advanced' === $active_tab ) { 71 | settings_fields( $this->plugin_name . '-settings-advanced' ); 72 | do_settings_sections( $this->plugin_name . '-settings-advanced' ); 73 | submit_button(); 74 | } elseif ( 'plugins' === $active_tab ) { 75 | do_settings_sections( $this->plugin_name . '-settings-plugins' ); 76 | } else { 77 | settings_fields( $this->plugin_name . '-settings' ); 78 | do_settings_sections( $this->plugin_name . '-settings' ); 79 | submit_button(); 80 | } 81 | ?> 82 |
83 |
84 | -------------------------------------------------------------------------------- /.wordpress-org/icon-admin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 21 | 24 | 27 | 30 | 33 | 47 | 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 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 33 | 35 | 38 | 40 | 43 | 44 | 45 | 46 | 47 | 61 | 62 | -------------------------------------------------------------------------------- /public/partials/redirect.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | <?php esc_html_e( 'Redirecting...', $this->plugin_name ); ?> 23 | 84 | 85 | 86 | 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 = '' . $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 | --------------------------------------------------------------------------------