diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..f728750 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,29 @@ +# WordPress version can be any of the listed tags from https://hub.docker.com/_/wordpress/tags +# E.g. latest, 6.2, ... +ARG WORDPRESS_VERSION=latest + +# PHP Variant can be any of the listed tags from https://mcr.microsoft.com/v2/devcontainers/php/tags/list +ARG PHP_VARIANT=8.3-bullseye + +FROM wordpress:${WORDPRESS_VERSION} +FROM mcr.microsoft.com/vscode/devcontainers/php:${PHP_VARIANT} + +# Copy WordPress files from wordpress container. +COPY --from=0 /usr/src/wordpress/ /var/www/html/ + +# Make vscode owner of all WordPress files. +RUN chown -R vscode:vscode /var/www/html + +# Install php-mysql driver +RUN docker-php-ext-install mysqli pdo pdo_mysql + +# Install additional packages for running e2e tests +RUN apt-get update && \ + apt-get install -y subversion && \ + apt-get install -y default-mysql-client + +# Enable apache mods +RUN a2enmod rewrite expires + +# Install WP-CLI +RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar && sudo mv wp-cli.phar /usr/local/bin/wp diff --git a/.devcontainer/config/_.env b/.devcontainer/config/_.env new file mode 100644 index 0000000..ea91fa3 --- /dev/null +++ b/.devcontainer/config/_.env @@ -0,0 +1 @@ +MUNICIPIO_ACF_PRO_KEY=b3JkZXJfaWQ9ODE2NDZ8dHlwZT1kZXZlbG9wZXJ8ZGF0ZT0yMDE2LTA1LTE2IDExOjEyOjQ4 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..7ac40a1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,58 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/php-mariadb +{ + "name": "PHP & MySQL", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "forwardPorts": [ + 80, + 3306 + ], + "portsAttributes": { + "80": { + "label": "WordPress" + }, + "3306": { + "label": "Database" + } + }, + "remoteEnv": { + "XDEBUG_MODE": "off" + }, + "customizations": { + "vscode": { + "extensions": [ + "xdebug.php-debug", + "ms-azuretools.vscode-docker", + "ritwickdey.liveserver" + ], + "settings": { + "intelephense.environment.includePaths": [ + "/var/www/html", + "/tmp/wordpress-tests-lib/includes" + ], + "intelephense.environment.phpVersion": "8.3" + } + } + }, + "features": { + "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "packages": "curl,nano,bash-completion", + "upgradePackages": true + }, + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "16" + } + }, + "postCreateCommand": { + "symlink-plugin": "ln -s \"$(pwd)\" \"/var/www/html/wp-content/plugins/$(basename \"$PWD\")\"", + "symlink-wp-config-local": "ln -s \"$(pwd)/.devcontainer/wp-config-local.php\" /var/www/html/wp-config-local.php", + "symlink-wp-config": "ln -s \"$(pwd)/.devcontainer/wp-config.php\" /var/www/html/wp-config.php", + "start-apache": "service apache2 start", + "setup-e2e-tests": "composer test:setup:e2e" + }, + "waitFor": "postCreateCommand", + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..25a3ce5 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + app: + # env_file: devcontainer.env + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: mariadb:10.4 + restart: always + volumes: + - db-data:/var/lib/mysql + environment: + MYSQL_DATABASE: dev + MYSQL_ROOT_PASSWORD: dev + MYSQL_USER: dev + MYSQL_PASSWORD: dev + + # Add "forwardPorts": ["3306"] to **devcontainer.json** to forward MariaDB locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + +volumes: + db-data: \ No newline at end of file diff --git a/.devcontainer/wp-config.php b/.devcontainer/wp-config.php new file mode 100644 index 0000000..6d72d38 --- /dev/null +++ b/.devcontainer/wp-config.php @@ -0,0 +1,106 @@ +HHW}D6T#6ktdvZ4qMpm7OW+vR:pHxm'); +define('SECURE_AUTH_KEY', 'pPwN{M1g=?j`me{7rE-7OtJN$zDS(y#uM7.1~.X~>m+#/ln~%Myo$Ca2mBFEa0s:'); +define('LOGGED_IN_KEY', '8-AQ.XlAXQTq8:f2ddzxIs]xeC cE`ZdvAv9S=*bg>I~bHT/jGLyXW4=)!rhjz+g'); +define('NONCE_KEY', '9{1MgwMd|^-i3OIy(`4jmEk5T/%1Q_i^bB&h%Jc7: :m]Ic7e&fkc=8YV_2|A~Fs'); +define('AUTH_SALT', 'P(}]`j]rI]RzdS*{U]Y#(n~NzR|*Xx)6{0*/kvY^pc'); + +/**#@-*/ + +/** + * WordPress database table prefix. + * + * You can have multiple installations in one database if you give each + * a unique prefix. Only numbers, letters, and underscores please! + */ +if (!isset($table_prefix)) $table_prefix = 'wp_'; + +/** + * For developers: WordPress debugging mode. + * + * Change this to true to enable the display of notices during development. + * It is strongly recommended that plugin and theme developers use WP_DEBUG + * in their development environments. + * + * For information on other constants that can be used for debugging, + * visit the documentation. + * + * @link https://wordpress.org/documentation/article/debugging-in-wordpress/ + */ +define('WP_DEBUG', true); +if (!defined('WP_DEBUG')) define('WP_DEBUG', false); + +define('WP_DEBUG_LOG', true); + +/* Add any custom values between this line and the "stop editing" line. */ + + + +/* That's all, stop editing! Happy publishing. */ + +/** Absolute path to the WordPress directory. */ +if (!defined('ABSPATH')) define('ABSPATH', __DIR__ . '/'); + +/** Sets up WordPress vars and included files. */ +require_once ABSPATH . 'wp-settings.php'; \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..5ca21e7 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,50 @@ +name: Build assets and create a release on version tags + +on: + push: + tags: + - 'v*.*.*' +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Setup PHP with composer v2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2 + + - name: Inject access token in .npmrc + run: | + echo "registry=https://npm.pkg.github.com/helsingborg-stad" >> ~/.npmrc + echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> ~/.npmrc + + - name: Run full build. + run: php ./build.php --cleanup + + - name: Cleanup .npmrc + run: rm ~/.npmrc + + - name: Archive Release + uses: thedoctor0/zip-release@master + with: + filename: 'full-release.zip' + + - name: Release + uses: docker://antonyurchenko/git-release:latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRAFT_RELEASE: "false" + PRE_RELEASE: "false" + CHANGELOG_FILE: "none" + with: + args: | + full-release.zip \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2d556a..8aef241 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Create Release and bump version files uses: helsingborg-stad/release-wp-plugin-action@1.0.2 with: - php-version: 8.2 + php-version: 8.3 node-version: 20.6.0 build-assets: needs: ['release'] @@ -45,7 +45,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 20.6.0 + node-version: 20 - name: Inject access token in .npmrc run: | echo "registry=https://npm.pkg.github.com/helsingborg-stad" >> ~/.npmrc @@ -83,7 +83,7 @@ jobs: if: ${{ hashFiles('composer.json') != '' }} with: tools: composer - php-version: '7.4' + php-version: '8.3' - name: Build PHP if: ${{ hashFiles('composer.json') != '' }} diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml new file mode 100644 index 0000000..2ebf10c --- /dev/null +++ b/.github/workflows/test-js.yml @@ -0,0 +1,30 @@ +name: Test JS + +on: + push: + branches: [ 3.0/develop, master ] + pull_request: + branches: [ 3.0/develop, master ] + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Cache node modules + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '16.x' + - name: Install dependencies + run: npm ci + - name: Run tests + run: npm test \ No newline at end of file diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml new file mode 100644 index 0000000..211ed82 --- /dev/null +++ b/.github/workflows/test-php.yml @@ -0,0 +1,29 @@ +name: Test:PHP + +on: + pull_request: + +jobs: + + test: + + runs-on: ubuntu-latest + + steps: + + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP with composer v2 + uses: shivammathur/setup-php@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + php-version: '8.2' + tools: composer:v2 + + - name: Install dependencies + run: composer i + + - name: Run tests + run: composer test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8e10e3f..2dbbd53 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,24 @@ # ignore node/grunt dependency directories node_modules/ +# ignore vendor +vendor/ + # ignore composer lock file composer.lock +# ignore .env file +.env + +# ignore .DS_Store files +.DS_Store + +# ignore .vscode files +.vscode/ + +# ignore dist files +/dist/* + # ignore jekyll build directory /_site @@ -51,3 +66,4 @@ Thumbs.db # track these files, if they exist !.gitignore !.editorconfig +!.devcontainer diff --git a/Public.php b/Public.php deleted file mode 100644 index a2451f4..0000000 --- a/Public.php +++ /dev/null @@ -1,21 +0,0 @@ - Broken links summary +This page presents a summary of all links that was found to be broken in the last classification. This page is avabile for users that can edit posts. + +## Settings > Broken links settings +This page gives an administrator the ability to configure the plugin. A whitelist of domains is provided, and the ability to enable context detection feature. + +# Cli documentation +Broken link detector does not rely on scheduled actions due to its resource intensive nature. Instead a set of cli actions is provided to maintain the link registry. + +## Cli commands +All broken links cli commands are placed under **broken-link-detector** prefix. To get a up to date index of all options please use the following command: + +```wp broken-link-detector --info`` + +### Install, Uninstall & Reinstall +This command allows you to install, reinstall or uninstall the database table required for broken link registry. + +```wp broken-link-detector database --[install, uninstall, reinstall]``` + +### Find links +This command will scan your sites content and meta data for links, and register them in the link registry. The links will not show up in the summary, util they have been classified as broken. The flags in this command is optional, and will default to true. + +```wp broken-link-detector find-links --meta=true --content=true``` + +### Classify Links +This command will asses and classify each found link to check if the link is valid or not. This is a resource intensive action, therefore a limit can be applied to classify a subset of links. This function utilizes the confuration value in recheckInterval, to prevent calls to frequent of external services. + +```wp broken-link-detector classify-links --limit=[NUMBER]``` + + +# Filter Documentation + +This document provides an overview of the available filters within the `BrokenLinkDetector\Config` class. All filters are prefixed with `BrokenLinkDetector/Config`. + +## Filters + +### `BrokenLinkDetector/Config/getDatabaseVersionKey` + +**Description:** +Filter the key used for the database version. + +**Default Value:** +`'broken_link_detector_db_version'` + +### `BrokenLinkDetector/Config/getDatabaseVersion` + +**Description:** +Filter the current database version from the options table. + +**Default Value:** +`'2.0.0'` + +### `BrokenLinkDetector/Config/getTableName` + +**Description:** +Filter the name of the table that stores broken links. + +**Default Value:** +`'broken_links_detector'` + +### `BrokenLinkDetector/Config/getPluginUrl` + +**Description:** +Filter the plugin URL. + +**Default Value:** +The value provided during object construction for `pluginUrl`. + +### `BrokenLinkDetector/Config/getPluginPath` + +**Description:** +Filter the plugin path. + +**Default Value:** +The value provided during object construction for `pluginPath`. + +### `BrokenLinkDetector/Config/getPluginFieldsPath` + +**Description:** +Filter the path where fields are located. + +**Default Value:** +The plugin path appended with `source/fields`. + +### `BrokenLinkDetector/Config/getTextDomain` + +**Description:** +Filter the text domain. + +**Default Value:** +`'broken-link-detector'` + +### `BrokenLinkDetector/Config/linkUpdaterBannedPostTypes` + +**Description:** +Filter the post types where link repair (link updater) should not run. + +**Default Value:** +`['attachment', 'revision', 'acf', 'acf-field', 'acf-field-group']` + +### `BrokenLinkDetector/Config/linkDetectBannedPostTypes` + +**Description:** +Filter the post types that should not be checked for broken links. + +**Default Value:** +`['attachment', 'revision', 'acf', 'acf-field', 'acf-field-group']` + +### `BrokenLinkDetector/Config/linkDetectAllowedPostStatuses` + +**Description:** +Filter the post types that should not be checked for broken links based on status. + +**Default Value:** +`['publish', 'private', 'password']` + +### `BrokenLinkDetector/Config/responseCodesConsideredBroken` + +**Description:** +Filter the response codes that are considered broken. + +**Default Value:** +`[400, 403, 404, 410, 500, 502, 503, 504]` + +### `BrokenLinkDetector/Config/checkIfDnsRespondsBeforeProbingUrl` + +**Description:** +Filter to determine if DNS should respond before probing the URL. + +**Default Value:** +`true` + +### `BrokenLinkDetector/Config/getMaxRedirects` + +**Description:** +Filter the number of redirects to follow. + +**Default Value:** +`5` + +### `BrokenLinkDetector/Config/getTimeout` + +**Description:** +Filter the timeout for the request. + +**Default Value:** +`5` + +### `BrokenLinkDetector/Config/getRecheckInterval` + +**Description:** +Filter the interval for rechecking broken links in minutes. + +**Default Value:** +`720` + +### `BrokenLinkDetector/Config/getDomainsThatShouldNotBeChecked` + +**Description:** +Filter the domains that should not be checked for broken links. These will be registered, but always return `null`. + +**Default Value:** +An empty array if no domains are set in ACF fields. + +### `BrokenLinkDetector/Config/isContextCheckEnabled` + +**Description:** +Filter to enable/disable context check based on configuration and URL. + +**Default Value:** +`false` + +### `BrokenLinkDetector/Config/getContextCheckUrl` + +**Description:** +Filter the URL to probe for the context check. + +**Default Value:** +The value from the ACF field or an empty string if not set. + +### `BrokenLinkDetector/Config/getContextCheckTimeout` + +**Description:** +Filter the timeout in milliseconds for the context check. + +**Default Value:** +`3000` + +### `BrokenLinkDetector/Config/getContextCheckDomainsToDisable` + +**Description:** +Filter the domains that should be disabled when the context check fails. + +**Default Value:** +The domains retrieved by the `getDomainsThatShouldNotBeChecked` method. + +### `BrokenLinkDetector/Config/getContextCheckSuccessClass` + +**Description:** +Filter the class to be applied for a successful context check. + +**Default Value:** +`'context-check-avabile'` + +### `BrokenLinkDetector/Config/getContextCheckFailedClass` + +**Description:** +Filter the class to be applied for a failed context check. + +**Default Value:** +`'context-check-unavabile'` + +### `BrokenLinkDetector/Config/getContextCheckTooltipText` + +**Description:** +Filter the tooltip text for a disabled link due to context failure. + +**Default Value:** +The value from the ACF field or `'Link unavabile'` if not set. + +### `BrokenLinkDetector/Config/getCommandNamespace` + +**Description:** +Filter the namespace for the WP CLI command. + +**Default Value:** +`'broken-link-detector'` + +## Example Usage + +To modify the filter for the database version key, you can use the following code in your plugin or theme: ```php -add_filter('brokenLinks/External/ExceptedDomains',function($array) { - return array( - parse_url("http://agresso/agresso/", PHP_URL_HOST), - parse_url("http://qlikviewserver/qlikview/index.htm", PHP_URL_HOST), - parse_url("http://serviceportalen/", PHP_URL_HOST), - parse_url("http://a002163:81/login/login.asp", PHP_URL_HOST), - parse_url("http://serviceportalen/Default.aspx", PHP_URL_HOST), - parse_url("http://cmg/BluStarWeb/Start", PHP_URL_HOST), - parse_url("http://surveyreport/admin", PHP_URL_HOST), - parse_url("http://klarspraket/", PHP_URL_HOST), - parse_url("http://guideochtips/", PHP_URL_HOST), - parse_url("http://hbgquiz/index.php/category/?id=3", PHP_URL_HOST), - parse_url("http://agresso/agresso/", PHP_URL_HOST), - parse_url("http://a002490/efact/", PHP_URL_HOST), - parse_url("http://a002064/Kurser/", PHP_URL_HOST), - parse_url("http://a002064/kursbokning/", PHP_URL_HOST) - ); -}, 10); -``` \ No newline at end of file +add_filter('BrokenLinkDetector/Config/getDatabaseVersionKey', function($versionKey) { + return 'custom_version_key'; +}); diff --git a/broken-link-detector.php b/broken-link-detector.php index eb9e5b8..6be3587 100644 --- a/broken-link-detector.php +++ b/broken-link-detector.php @@ -2,41 +2,90 @@ /** * Plugin Name: Broken Link Detector - * Plugin URI: (#plugin_url#) * Description: Detects and fixes (if possible) broken links in post_content * Version: 3.0.6 - * Author: Kristoffer Svanmark - * Author URI: (#plugin_author_url#) + * Author: Sebastian Thulin * License: MIT * License URI: https://opensource.org/licenses/MIT * Text Domain: broken-link-detector * Domain Path: /languages */ - // Protect agains direct file access +use AcfService\Implementations\NativeAcfService; +use WpService\Implementations\NativeWpService; +use BrokenLinkDetector\Database\Database; +use BrokenLinkDetector\Config\Config; +use BrokenLinkDetector\BrokenLinkRegistry\Registry\ManageRegistry; +use BrokenLinkDetector\Cli\CommandRunner; + +/* Assets */ +use WpService\FileSystem\BaseFileSystem; +use WpService\FileSystemResolvers\ManifestFilePathResolver; +use WpService\FileSystemResolvers\UrlFilePathResolver; +use WpService\Implementations\FilePathResolvingWpService; +use WpService\Implementations\WpServiceLazyDecorator; +use WpService\Implementations\WpServiceWithTextDomain; + +/** + * If this file is called directly, abort. + */ if (! defined('WPINC')) { die; } -define('BROKENLINKDETECTOR_PATH', plugin_dir_path(__FILE__)); -define('BROKENLINKDETECTOR_URL', plugins_url('', __FILE__)); -define('BROKENLINKDETECTOR_TEMPLATE_PATH', BROKENLINKDETECTOR_PATH . 'templates/'); +/** + * Autoload plugin classes, if dependencies are installed. + */ +try { + require_once __DIR__ . '/vendor/autoload.php'; +} catch (Exception $e) { + throw new Exception($e->getMessage()); +} + +/** + * Bootstrap the plugin + */ +$wpService = new NativeWpService(); +$manifestFileWpService = new WpServiceLazyDecorator(); +$urlFilePathResolver = new UrlFilePathResolver($manifestFileWpService); +$baseFileSystem = new BaseFileSystem(); + +$acfService = new NativeAcfService(); -load_plugin_textdomain('broken-link-detector', false, plugin_basename(dirname(__FILE__)) . '/languages'); +$config = new Config( + $wpService, + $acfService, + 'BrokenLinkDetector/Config', + $wpService->pluginDirPath(__FILE__), + $wpService->pluginsUrl('', __FILE__) +); -require_once __DIR__ . '/source/php/Vendor/admin-notice-helper.php'; -require_once BROKENLINKDETECTOR_PATH . 'source/php/Vendor/Psr4ClassLoader.php'; -require_once BROKENLINKDETECTOR_PATH . 'Public.php'; -require_once BROKENLINKDETECTOR_PATH . 'vendor/autoload.php'; +$manifestFilePathResolver = new ManifestFilePathResolver( + $config->getPluginPath() . "dist/manifest.json", + $baseFileSystem, + $manifestFileWpService, + $urlFilePathResolver +); -// Instantiate and register the autoloader -$loader = new BrokenLinkDetector\Vendor\Psr4ClassLoader(); -$loader->addPrefix('BrokenLinkDetector', BROKENLINKDETECTOR_PATH); -$loader->addPrefix('BrokenLinkDetector', BROKENLINKDETECTOR_PATH . 'source/php/'); -$loader->register(); +$wpService = new FilePathResolvingWpService( + new NativeWpService(), + $manifestFilePathResolver +); -// Start application -$brokenLinkDetectorApp = new BrokenLinkDetector\App(); +$manifestFileWpService->setInner(new WpServiceWithTextDomain($wpService, $config->getTextDomain())); -register_activation_hook(__FILE__, '\BrokenLinkDetector\App::install'); -register_deactivation_hook(__FILE__, '\BrokenLinkDetector\App::uninstall'); +$database = new Database($config, $wpService); +$registry = new ManageRegistry($database, $config); +$cliRunner = new CommandRunner($wpService, $config); + +/** + * Run the plugin + */ +$brokenLinkDetectorApp = new BrokenLinkDetector\App( + $manifestFileWpService, + $acfService, + $database, + $registry, + $config, + $cliRunner +); \ No newline at end of file diff --git a/build.php b/build.php new file mode 100644 index 0000000..eafff78 --- /dev/null +++ b/build.php @@ -0,0 +1,132 @@ +#!/bin/php + 0) { + exit($exitCode); + } +} + +// Remove files and directories if '--cleanup' argument is supplied to save local developers from disasters. +if(is_array($argv) && in_array('--cleanup', $argv)) { + foreach ($removables as $removable) { + if (file_exists($removable)) { + print "Removing $removable from $dirName\n"; + shell_exec("rm -rf $removable"); + } + } +} + +/** + * Better shell script execution with live output to STDOUT and status code return. + * @param string $command Command to execute in shell. + * @return int Exit code. + */ +function executeCommand($command) +{ + $fullCommand = ''; + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $fullCommand = "cmd /v:on /c \"$command 2>&1 & echo Exit status : !ErrorLevel!\""; + } else { + $fullCommand = "$command 2>&1 ; echo Exit status : $?"; + } + + $proc = popen($fullCommand, 'r'); + + $liveOutput = ''; + $completeOutput = ''; + + while (!feof($proc)) { + $liveOutput = fread($proc, 4096); + $completeOutput = $completeOutput . $liveOutput; + print $liveOutput; + @ flush(); + } + + pclose($proc); + + // Get exit status. + preg_match('/[0-9]+$/', $completeOutput, $matches); + + // Return exit status. + return intval($matches[0]); +} \ No newline at end of file diff --git a/composer.json b/composer.json index 7a75e24..2835b52 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,83 @@ { "name": "helsingborg-stad/broken-link-detector", - "description": "Detects and fixes (if possible) broken links in post_content", + "description": "Detects broken links", "type": "wordpress-plugin", "license": "MIT", + "scripts": { + "test": "XDEBUG_MODE=off phpunit -c phpunit.default.xml --testdox", + "test:debug": "XDEBUG_MODE=debug ./vendor/bin/phpunit --testdox --no-coverage", + "test:setup:e2e": "./tests/install-wp-tests.sh test root dev 127.0.0.1", + "test:e2e": "phpunit -c phpunit.e2e.xml --testdox", + "coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --testdox", + "minimal": "./vendor/bin/phpunit", + "php:lint": "./vendor/bin/phpcs -s", + "php:fix": "./vendor/bin/phpcbf", + "php:analyze": "vendor/bin/phpstan analyse -c phpstan.neon" + }, + "extra": { + "hooks": { + "commit-msg": [ + "./bin/commit-msg.sh $1" + ] + }, + "merge-plugin": { + "include": [ + "composer.local.json" + ], + "ignore-duplicates": false, + "merge-dev": true, + "merge-extra": true, + "merge-scripts": true + } + }, "authors": [ { - "name": "Kristoffer Svanmark", - "email": "kristoffer.svanmark@lexiconitkonsult.se" + "name": "Sebastian Thulin", + "email": "sebastian.thulin@helsingborg.se" + }, + { + "name": "Niclas Norin", + "email": "niclas.norin@helsingborg.se" + }, + { + "name": "Thor Brink", + "email": "thor.brink@helsingborg.se" } ], + "autoload": { + "psr-4": { + "BrokenLinkDetector\\": "source/php/" + } + }, "require": { - "symfony/polyfill-intl-idn": "~1.2" + "php": ">=8.1", + "helsingborg-stad/acf-export-manager": ">=1.0.0", + "symfony/polyfill-intl-idn": "1.31.0", + "helsingborg-stad/wpservice": "^2.0", + "helsingborg-stad/acfservice": "^0.8.1" + }, + "require-dev": { + "codedungeon/phpunit-result-printer": "^0.32.0", + "squizlabs/php_codesniffer": "^3.7", + "wp-coding-standards/wpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "*", + "brainmaestro/composer-git-hooks": "^2.8", + "composer/installers": "~1.0", + "php-mock/php-mock-mockery": "^1.4", + "wikimedia/composer-merge-plugin": "^2.1", + "phpstan/phpstan": "2.0.x-dev", + "johnpbloch/wordpress-core": "dev-master", + "phpunit/phpunit": "^9.6", + "yoast/phpunit-polyfills": "^3.0", + "wp-cli/wp-cli": "^2.11" + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "wikimedia/composer-merge-plugin": true, + "johnpbloch/wordpress-core-installer": true + } }, "version": "3.0.6" -} \ No newline at end of file +} diff --git a/dist/css/broken-link-detector.fdba7e11c160efd580f5.css b/dist/css/broken-link-detector.fdba7e11c160efd580f5.css deleted file mode 100644 index 54631fc..0000000 --- a/dist/css/broken-link-detector.fdba7e11c160efd580f5.css +++ /dev/null @@ -1,2 +0,0 @@ -.broken-link-detector-label{background-color:red;border-radius:5px;box-sizing:border-box;color:#fff;display:inline-block;font-size:11px;height:2em;line-height:21px;margin-top:5px;min-width:24px;padding:0 8px;text-align:center} -/*# sourceMappingURL=broken-link-detector.fdba7e11c160efd580f5.css.map*/ \ No newline at end of file diff --git a/dist/css/broken-link-detector.fdba7e11c160efd580f5.css.map b/dist/css/broken-link-detector.fdba7e11c160efd580f5.css.map deleted file mode 100644 index 8b6b4c4..0000000 --- a/dist/css/broken-link-detector.fdba7e11c160efd580f5.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"css/broken-link-detector.fdba7e11c160efd580f5.css","mappings":"AAAA,4BACI,qBACA,kBACA,sBACA,WACA,qBACA,eACA,WACA,iBAIA,eAHA,eACA,cACA,iBACA","sources":["webpack://@helsingborg-stad/broken-link-detector/./source/sass/broken-link-detector.scss"],"sourcesContent":[".broken-link-detector-label {\n background-color: #ff0000;\n border-radius: 5px;\n box-sizing: border-box;\n color: #fff;\n display: inline-block;\n font-size: 11px;\n height: 2em;\n line-height: 21px;\n min-width: 24px;\n padding: 0 8px;\n text-align: center;\n margin-top: 5px;\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/dist/js/broken-link-detector.31d6cfe0d16ae931b73c.js b/dist/js/broken-link-detector.31d6cfe0d16ae931b73c.js deleted file mode 100644 index e69de29..0000000 diff --git a/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js b/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js deleted file mode 100644 index bdc7440..0000000 --- a/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js +++ /dev/null @@ -1,2 +0,0 @@ -tinymce.PluginManager.add("brokenlinksdetector",(function(e,n){var t='a[href="#"]',o='a[href="#"]::before',a='a[href="#"]::after';jQuery.each(broken_links,(function(e,n){t=t+', a[data-mce-href="'+n+'"]',o=o+', a[data-mce-href="'+n+'"]::before',a=a+', a[data-mce-href="'+n+'"]::after'})),e.contentStyles.push(t+" { color: #ff0000; text-decoration-style: wavy; }"),e.contentStyles.push(o+' { display: inline-block; margin-right: 3px; content: ""; font-family: dashicons; position: relative; top: 4px; text-dociration: none; }'),e.contentStyles.push(a+' { display: inline-block; margin-left: 3px; content: "Broken link"; font-family: arial; font-size: 12px; background-color: #ff0000; color: #fff; border-radius: 3px; padding: 0 4px; position: relative; top: -1px; }')})); -//# sourceMappingURL=mce-broken-link-detector.f59b3e265be67daef9ba.js.map \ No newline at end of file diff --git a/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js.map b/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js.map deleted file mode 100644 index e7fedc6..0000000 --- a/dist/js/mce-broken-link-detector.f59b3e265be67daef9ba.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"js/mce-broken-link-detector.f59b3e265be67daef9ba.js","mappings":"AACIA,QAAQC,cAAcC,IAAI,uBAAuB,SAASC,EAAQC,GAC9D,IAAIC,EAAY,cACZC,EAAmB,sBACnBC,EAAkB,qBAGtBC,OAAOC,KAAKC,cAAc,SAAUC,EAAOC,GACvCP,EAAYA,EAAY,sBAAwBO,EAAO,KACvDN,EAAmBA,EAAmB,sBAAwBM,EAAO,aACrEL,EAAkBA,EAAkB,sBAAwBK,EAAO,WACvE,IAEAT,EAAOU,cAAcC,KAAKT,EAAY,qDACtCF,EAAOU,cAAcC,KAAKR,EAAmB,6IAC7CH,EAAOU,cAAcC,KAAKP,EAAkB,wNAChD","sources":["webpack://@helsingborg-stad/broken-link-detector/./source/mce/mce-broken-link-detector.js"],"sourcesContent":["(function() {\n tinymce.PluginManager.add('brokenlinksdetector', function(editor, url) {\n var selectors = 'a[href=\"#\"]';\n var selectors_before = 'a[href=\"#\"]::before';\n var selectors_after = 'a[href=\"#\"]::after';\n\n // Ajax to get broken links for this post\n jQuery.each(broken_links, function (index, item) {\n selectors = selectors + ', a[data-mce-href=\"' + item + '\"]';\n selectors_before = selectors_before + ', a[data-mce-href=\"' + item + '\"]::before';\n selectors_after = selectors_after + ', a[data-mce-href=\"' + item + '\"]::after';\n });\n\n editor.contentStyles.push(selectors + \" { color: #ff0000; text-decoration-style: wavy; }\");\n editor.contentStyles.push(selectors_before + ' { display: inline-block; margin-right: 3px; content: \"\"; font-family: dashicons; position: relative; top: 4px; text-dociration: none; }');\n editor.contentStyles.push(selectors_after + ' { display: inline-block; margin-left: 3px; content: \"Broken link\"; font-family: arial; font-size: 12px; background-color: #ff0000; color: #fff; border-radius: 3px; padding: 0 4px; position: relative; top: -1px; }');\n });\n})();\n\n"],"names":["tinymce","PluginManager","add","editor","url","selectors","selectors_before","selectors_after","jQuery","each","broken_links","index","item","contentStyles","push"],"sourceRoot":""} \ No newline at end of file diff --git a/dist/manifest.json b/dist/manifest.json index 0c7c2f1..eec7bb1 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -1,5 +1,5 @@ { - "css/broken-link-detector.css": "css/broken-link-detector.fdba7e11c160efd580f5.css", - "js/broken-link-detector.js": "js/broken-link-detector.31d6cfe0d16ae931b73c.js", - "js/mce-broken-link-detector.js": "js/mce-broken-link-detector.f59b3e265be67daef9ba.js" + "css/broken-link-detector.css": "css/broken-link-detector.024cd1a77a7b5c050e6a.css", + "js/context-detector.js": "js/context-detector.93354089fe4a513a8445.js", + "js/editor-highlight.js": "js/editor-highlight.a7ccb59aeba3991ac935.js" } \ No newline at end of file diff --git a/languages/broken-link-detector-sv_SE.mo b/languages/broken-link-detector-sv_SE.mo index 63ce2ef..b618fcd 100644 Binary files a/languages/broken-link-detector-sv_SE.mo and b/languages/broken-link-detector-sv_SE.mo differ diff --git a/languages/broken-link-detector-sv_SE.po b/languages/broken-link-detector-sv_SE.po index 521d2cf..3b6bc30 100644 --- a/languages/broken-link-detector-sv_SE.po +++ b/languages/broken-link-detector-sv_SE.po @@ -1,70 +1,432 @@ msgid "" msgstr "" "Project-Id-Version: Broken Link Detector\n" -"POT-Creation-Date: 2019-02-26 10:40+0100\n" -"PO-Revision-Date: 2019-02-26 10:42+0100\n" +"POT-Creation-Date: 2024-11-13 10:53+0100\n" +"PO-Revision-Date: 2024-11-13 11:03+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: sv_SE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.8.12\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.5\n" "X-Poedit-Basepath: ..\n" "X-Poedit-WPHeader: broken-link-detector.php\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;" "esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;" "_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\n" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPathExcluded-0: *.js\n" +"X-Poedit-SearchPathExcluded-1: dist\n" +"X-Poedit-SearchPathExcluded-2: vendor\n" +"X-Poedit-SearchPathExcluded-3: node_modules\n" + +#: source/fields/php/context-detection.php:6 +msgid "Context Detect" +msgstr "Identifiering av användarkontext" -#: source/php/App.php:55 -msgid "Rescan broken links" +#: source/fields/php/context-detection.php:10 +msgid "Enable detection of user context" +msgstr "Identifiera användarens kontext i nätverket" + +#: source/fields/php/context-detection.php:14 +msgid "" +"The user detection functionality will disable links that are internal only. " +"It also adds a tipbox to them explaining why the link is unreachable." msgstr "" +"Den här funktionen identifierar om användaren har tillgång till interna " +"system i nätverket genom att efterfråga en resurs. Länkar som inte är " +"tillgängliga om denna resurs är otillgänglig, kommer markeras som icke " +"klickbara." -#: source/php/App.php:56 +#: source/fields/php/context-detection.php:22 +msgid "Detects user context by fetching a internal resource." +msgstr "" +"Identifierar användarens kontext i nätverket genom att försöka hämta en " +"intern resurs." + +#: source/fields/php/context-detection.php:24 +msgid "Enabled" +msgstr "Aktiverad" + +#: source/fields/php/context-detection.php:25 +msgid "Disabled" +msgstr "Avaktiverad" + +#: source/fields/php/context-detection.php:30 +msgid "Internal Context Detection Resource" +msgstr "Resurs som ska hämtas" + +#: source/fields/php/context-detection.php:34 msgid "" -"The rescan will be executed for this post only. The scan will execute " -"direcly after the save is completed and may take a few minutes to complete." +"The internal context checker require you to publish a image on a server " +"without public access. The image should be as small as possible, if your " +"site is running on https, this resource must also be served with https." msgstr "" -"Scanningen efter brutna länkar kommer bara att genomföras på det här " -"inläget. Genomsökningen körs direk efter att inlägget sparas, men kan ta " -"ett par minuter innan det presenteras i panelen. " +"För att identifiera användarens kontext, krävs det att du publicerar en " +"bild på en webbadress som bara kan nås från det interna nätverket. Resursen " +"måste vara tillgänglig via http eller https (om din webbplats använder ssl-" +"certifikat)." + +#: source/fields/php/context-detection.php:51 +msgid "https://internal.resource.admin-network.local/image-1x1.jpg" +msgstr "" + +#: source/fields/php/context-detection.php:55 +msgid "Tooltip Text" +msgstr "Text på tooltip" + +#: source/fields/php/context-detection.php:59 +msgid "The text that displays in the tooltip, whenever a link is unavabile." +msgstr "Den text som ska visas när en länk inte är tillgänglig." + +#: source/fields/php/context-detection.php:77 source/php/Config/Config.php:340 +msgid "Link unavabile" +msgstr "Otillgänglig" + +#: source/fields/php/local-domains.php:6 +msgid "Local Domain Settings" +msgstr "Inställningar för lokala domäner" + +#: source/fields/php/local-domains.php:10 +msgid "Local domains" +msgstr "Lokala domäner" + +#: source/fields/php/local-domains.php:14 +msgid "Add domains in this list, that should not be checked." +msgstr "" +"Lägg till domäner som ska hanteras som interna resurser. Dom här länkarna " +"kommer inte att dyka upp i summeringen för brutna länkar, och kommer " +"markeras som otillgängliga om du har aktiverat funktionen för att " +"identifiera användarens kontext." + +#: source/fields/php/local-domains.php:28 +msgid "Lägg till rad" +msgstr "Lägg till rad" + +#: source/fields/php/local-domains.php:33 +msgid "Domain" +msgstr "Domän" -#: source/php/App.php:103 templates/list-table.php:2 -msgid "Broken links" +#: source/fields/php/local-domains.php:37 +msgid "eg. https://domain.com or https://subdomain.domain.com" +msgstr "t.ex https://domain.com eller https://subdoman.domain.com" + +#: source/php/Admin/Settings/SettingsPage.php:58 +#: source/php/Admin/Settings/SettingsPage.php:60 +msgid "Broken Links Settings" +msgstr "Inställningar för brutna länkar" + +#: source/php/Admin/Settings/SettingsPage.php:68 +msgid "Update" +msgstr "Uppdatera" + +#: source/php/Admin/Settings/SettingsPage.php:69 +msgid "Settings updated" +msgstr "Inställningarna uppdaterades" + +#: source/php/Admin/Summary/OptionsPage.php:32 +#: source/php/Admin/Summary/OptionsPage.php:33 +#: source/php/Admin/Summary/OptionsPage.php:43 +msgid "Broken Links Report" +msgstr "Rapport av brutna länkar" + +#: source/php/Admin/Summary/OptionsPage.php:44 +msgid "Here is a summary of broken links found in your content." +msgstr "Här är en summering av länkar som har identifierats som brutna." + +#: source/php/Admin/Summary/Table.php:14 +msgid "Broken Link" +msgstr "Brutna länkar" + +#: source/php/Admin/Summary/Table.php:15 +msgid "Broken Links" msgstr "Brutna länkar" -#: source/php/ListTable.php:85 +#: source/php/Admin/Summary/Table.php:70 msgid "Post" msgstr "Inlägg" -#: source/php/ListTable.php:86 -msgid "Web adress" -msgstr "Webbadress" +#: source/php/Admin/Summary/Table.php:71 +msgid "URL" +msgstr "URL" + +#: source/php/Admin/Summary/Table.php:72 +msgid "HTTP Code" +msgstr "HTTP Kod" + +#: source/php/Admin/Summary/Table.php:73 +msgid "Last Checked" +msgstr "Senast kontrollerad" -#: source/php/ListTable.php:87 -msgid "Last probed" -msgstr "Senast testad" +#: source/php/Admin/Summary/Table.php:96 +msgid "View Post" +msgstr "Visa inlägg" + +#: source/php/Admin/Summary/Table.php:99 +msgid "N/A" +msgstr "Ej tillämpligt" + +#: source/php/Admin/Summary/Table.php:116 +msgid "All HTTP Codes" +msgstr "Alla HTTP Koder" + +#: source/php/Admin/Summary/Table.php:122 +msgid "Filter" +msgstr "Filter" #. Plugin Name of the plugin/theme msgid "Broken Link Detector" msgstr "Brutna länkar (Broken Link Detector)" -#. Plugin URI of the plugin/theme -msgid "(#plugin_url#)" -msgstr "" - #. Description of the plugin/theme msgid "Detects and fixes (if possible) broken links in post_content" msgstr "Testar och åtgärdar brutna länkar i innehållsfältet" #. Author of the plugin/theme -msgid "Kristoffer Svanmark" +msgid "Sebastian Thulin" msgstr "" -#. Author URI of the plugin/theme -msgid "(#plugin_author_url#)" -msgstr "" +#, fuzzy +#~| msgid "Web adress" +#~ msgid "Web Address" +#~ msgstr "Webbadress" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "post type general name" +#~ msgid "Posts" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Used" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Web adress" +#~ msgid "WordPress" +#~ msgstr "Webbadress" + +#, fuzzy +#~| msgid "Web adress" +#~ msgid "Web server" +#~ msgstr "Webbadress" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last page" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Updated" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Updated:" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Modified" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Broken links" +#~ msgid "Broken Hill" +#~ msgstr "Brutna länkar" + +#, fuzzy, php-format +#~| msgid "Post" +#~ msgid "%s Post" +#~ msgid_plural "%s Posts" +#~ msgstr[0] "Inlägg" +#~ msgstr[1] "Inlägg" + +#, fuzzy +#~| msgid "Web adress" +#~ msgid "Image Address" +#~ msgstr "Webbadress" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "last page" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post name" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Broken links" +#~ msgid "Broken Themes" +#~ msgstr "Brutna länkar" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Name" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Posts" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "All Posts" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Edit Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "New Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post Link" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post ID" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post Type" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last Page" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "Used before publish date." +#~ msgid "Posted on" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Broken links" +#~ msgid "More links" +#~ msgstr "Brutna länkar" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Posted by" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Posted in" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Next Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Web adress" +#~ msgid "Web Design" +#~ msgstr "Webbadress" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post date" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "" +#~ "Prefix before the author name. The post atuhor name is displayed in a " +#~ "separate block on the next line." +#~ msgid "Posted by" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "Verb to explain the publication status of a post" +#~ msgid "Posted" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Latest posts" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "block bindings source" +#~ msgid "Post Meta" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "Block pattern category" +#~ msgid "Posts" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Poster" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Broken links" +#~ msgid "Remove link" +#~ msgstr "Brutna länkar" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "post type singular name" +#~ msgid "Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "navigation link block title" +#~ msgid "Post Link" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Last probed" +#~ msgid "Last updated" +#~ msgstr "Senast testad" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Last Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgctxt "add new from admin bar" +#~ msgid "Post" +#~ msgstr "Inlägg" + +#, fuzzy +#~| msgid "Post" +#~ msgid "Post ID." +#~ msgstr "Inlägg" + +#~ msgid "" +#~ "The rescan will be executed for this post only. The scan will execute " +#~ "direcly after the save is completed and may take a few minutes to " +#~ "complete." +#~ msgstr "" +#~ "Scanningen efter brutna länkar kommer bara att genomföras på det här " +#~ "inläget. Genomsökningen körs direk efter att inlägget sparas, men kan ta " +#~ "ett par minuter innan det presenteras i panelen. " diff --git a/package-lock.json b/package-lock.json index a2ab2e4..fb93f32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "3.0.6", "license": "MIT", "devDependencies": { + "@types/node": "^22.7.9", + "@types/tinymce": "^4.6.9", "autoprefixer": "^10.4.2", "browserslist": "^4.23.3", "clean-webpack-plugin": "^4.0.0", @@ -20,6 +22,8 @@ "postcss-loader": "^6.2.1", "sass": "^1.49.7", "sass-loader": "^12.4.0", + "ts-loader": "^9.5.1", + "typescript": "^5.6.3", "webpack": "^5.68.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -164,6 +168,15 @@ "@types/node": "*" } }, + "node_modules/@types/jquery": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", + "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -177,9 +190,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", "dev": true, "dependencies": { "undici-types": "~6.19.2" @@ -191,6 +204,21 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true + }, + "node_modules/@types/tinymce": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/tinymce/-/tinymce-4.6.9.tgz", + "integrity": "sha512-pDxBUlV4v1jgJ97SlnVOSyf3KUy3OQ3s5Ddpfh1L9M5lXlBmX7TJ2OLSozx1WBxp91acHvYPWDwz2U/kMM1oxQ==", + "dev": true, + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -1942,6 +1970,19 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3366,6 +3407,109 @@ "node": ">=6" } }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 6691e87..4e39cff 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,28 @@ "type": "git", "url": "https://github.com/helsingborg-stad/broken-link-detector.git" }, - "author": "Kristoffer Svanmark", + "authors": [ + { + "name": "Sebastian Thulin", + "email": "sebastian.thulin@helsingborg.se" + }, + { + "name": "Niclas Norin", + "email": "niclas.norin@helsingborg.se" + }, + { + "name": "Thor Brink", + "email": "thor.brink@helsingborg.se" + } + ], "license": "MIT", "bugs": { "url": "https://github.com/helsingborg-stad/broken-link-detector/issues" }, "homepage": "https://github.com/helsingborg-stad/broken-link-detector", "devDependencies": { + "@types/node": "^22.7.9", + "@types/tinymce": "^4.6.9", "autoprefixer": "^10.4.2", "browserslist": "^4.23.3", "clean-webpack-plugin": "^4.0.0", @@ -38,6 +53,8 @@ "postcss-loader": "^6.2.1", "sass": "^1.49.7", "sass-loader": "^12.4.0", + "ts-loader": "^9.5.1", + "typescript": "^5.6.3", "webpack": "^5.68.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -46,4 +63,4 @@ "webpack-notifier": "^1.15.0", "webpack-remove-empty-scripts": "^0.7.3" } -} \ No newline at end of file +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..e18fc46 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,13 @@ + + + WordPress Coding Standard. + + ./source/php + ../fields/* + **/*.blade.php + + + + + + \ No newline at end of file diff --git a/phpunit.default.xml b/phpunit.default.xml new file mode 100644 index 0000000..a318820 --- /dev/null +++ b/phpunit.default.xml @@ -0,0 +1,34 @@ + + + + + + ./source + + + ./source/fields + + + + + + + + + + + source/php + + + + + + + \ No newline at end of file diff --git a/phpunit.e2e.xml b/phpunit.e2e.xml new file mode 100644 index 0000000..7520537 --- /dev/null +++ b/phpunit.e2e.xml @@ -0,0 +1,15 @@ + + + + + source/php + + + diff --git a/source/fields/json/context-detection.json b/source/fields/json/context-detection.json new file mode 100644 index 0000000..47537b5 --- /dev/null +++ b/source/fields/json/context-detection.json @@ -0,0 +1,105 @@ +[{ + "key": "group_6718e9e8554ca", + "title": "Context Detect", + "fields": [ + { + "key": "field_6718e9eed55b7", + "label": "Enable detection of user context", + "name": "broken_links_context_check_enabled", + "aria-label": "", + "type": "true_false", + "instructions": "The user detection functionality will disable links that are internal only. It also adds a tipbox to them explaining why the link is unreachable.", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "message": "Detects user context by fetching a internal resource.", + "default_value": 0, + "ui_on_text": "Enabled", + "ui_off_text": "Disabled", + "ui": 1 + }, + { + "key": "field_6718ea46d55b8", + "label": "Internal Context Detection Resource", + "name": "broken_links_context_check_url", + "aria-label": "", + "type": "url", + "instructions": "The internal context checker require you to publish a image on a server without public access. The image should be as small as possible, if your site is running on https, this resource must also be served with https.", + "required": 1, + "conditional_logic": [ + [ + { + "field": "field_6718e9eed55b7", + "operator": "==", + "value": "1" + } + ] + ], + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "placeholder": "https:\/\/internal.resource.admin-network.local\/image-1x1.jpg" + }, + { + "key": "field_6733096f5d072", + "label": "Tooltip Text", + "name": "broken_links_context_tooltip", + "aria-label": "", + "type": "text", + "instructions": "The text that displays in the tooltip, whenever a link is unavabile.", + "required": 0, + "conditional_logic": [ + [ + { + "field": "field_6718e9eed55b7", + "operator": "==", + "value": "1" + } + ] + ], + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "maxlength": "", + "placeholder": "Link unavabile", + "prepend": "", + "append": "" + } + ], + "location": [ + [ + { + "param": "options_page", + "operator": "==", + "value": "broken-links-settings" + } + ] + ], + "menu_order": 0, + "position": "normal", + "style": "default", + "label_placement": "left", + "instruction_placement": "label", + "hide_on_screen": "", + "active": true, + "description": "", + "show_in_rest": 0, + "acfe_display_title": "", + "acfe_autosync": [ + "json" + ], + "acfe_form": 0, + "acfe_meta": "", + "acfe_note": "" +}] + \ No newline at end of file diff --git a/source/fields/json/local-domains.json b/source/fields/json/local-domains.json new file mode 100644 index 0000000..51ce332 --- /dev/null +++ b/source/fields/json/local-domains.json @@ -0,0 +1,75 @@ +[{ + "key": "group_6718e7ca78c94", + "title": "Local Domain Settings", + "fields": [ + { + "key": "field_6718e7d0b54f7", + "label": "Local domains", + "name": "broken_links_local_domains", + "aria-label": "", + "type": "repeater", + "instructions": "Add domains in this list, that should not be checked.", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "acfe_repeater_stylised_button": 1, + "layout": "table", + "pagination": 0, + "min": 0, + "max": 0, + "collapsed": "", + "button_label": "L\u00e4gg till rad", + "rows_per_page": 20, + "sub_fields": [ + { + "key": "field_6718e860b54f9", + "label": "Domain", + "name": "domain", + "aria-label": "", + "type": "url", + "instructions": "eg. https:\/\/domain.com or https:\/\/subdomain.domain.com", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "placeholder": "", + "parent_repeater": "field_6718e7d0b54f7" + } + ] + } + ], + "location": [ + [ + { + "param": "options_page", + "operator": "==", + "value": "broken-links-settings" + } + ] + ], + "menu_order": 0, + "position": "normal", + "style": "default", + "label_placement": "left", + "instruction_placement": "label", + "hide_on_screen": "", + "active": true, + "description": "", + "show_in_rest": 0, + "acfe_display_title": "", + "acfe_autosync": [ + "json" + ], + "acfe_form": 0, + "acfe_meta": "", + "acfe_note": "" +}] + \ No newline at end of file diff --git a/source/fields/php/context-detection.php b/source/fields/php/context-detection.php new file mode 100644 index 0000000..963177e --- /dev/null +++ b/source/fields/php/context-detection.php @@ -0,0 +1,108 @@ + 'group_6718e9e8554ca', + 'title' => __('Context Detect', 'api-event-manager'), + 'fields' => array( + 0 => array( + 'key' => 'field_6718e9eed55b7', + 'label' => __('Enable detection of user context', 'api-event-manager'), + 'name' => 'broken_links_context_check_enabled', + 'aria-label' => '', + 'type' => 'true_false', + 'instructions' => __('The user detection functionality will disable links that are internal only. It also adds a tipbox to them explaining why the link is unreachable.', 'api-event-manager'), + 'required' => 0, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'message' => __('Detects user context by fetching a internal resource.', 'api-event-manager'), + 'default_value' => 0, + 'ui_on_text' => __('Enabled', 'api-event-manager'), + 'ui_off_text' => __('Disabled', 'api-event-manager'), + 'ui' => 1, + ), + 1 => array( + 'key' => 'field_6718ea46d55b8', + 'label' => __('Internal Context Detection Resource', 'api-event-manager'), + 'name' => 'broken_links_context_check_url', + 'aria-label' => '', + 'type' => 'url', + 'instructions' => __('The internal context checker require you to publish a image on a server without public access. The image should be as small as possible, if your site is running on https, this resource must also be served with https.', 'api-event-manager'), + 'required' => 1, + 'conditional_logic' => array( + 0 => array( + 0 => array( + 'field' => 'field_6718e9eed55b7', + 'operator' => '==', + 'value' => '1', + ), + ), + ), + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'placeholder' => __('https://internal.resource.admin-network.local/image-1x1.jpg', 'api-event-manager'), + ), + 2 => array( + 'key' => 'field_6733096f5d072', + 'label' => __('Tooltip Text', 'api-event-manager'), + 'name' => 'broken_links_context_tooltip', + 'aria-label' => '', + 'type' => 'text', + 'instructions' => __('The text that displays in the tooltip, whenever a link is unavabile.', 'api-event-manager'), + 'required' => 0, + 'conditional_logic' => array( + 0 => array( + 0 => array( + 'field' => 'field_6718e9eed55b7', + 'operator' => '==', + 'value' => '1', + ), + ), + ), + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'maxlength' => '', + 'placeholder' => __('Link unavabile', 'api-event-manager'), + 'prepend' => '', + 'append' => '', + ), + ), + 'location' => array( + 0 => array( + 0 => array( + 'param' => 'options_page', + 'operator' => '==', + 'value' => 'broken-links-settings', + ), + ), + ), + 'menu_order' => 0, + 'position' => 'normal', + 'style' => 'default', + 'label_placement' => 'left', + 'instruction_placement' => 'label', + 'hide_on_screen' => '', + 'active' => true, + 'description' => '', + 'show_in_rest' => 0, + 'acfe_display_title' => '', + 'acfe_autosync' => array( + 0 => 'json', + ), + 'acfe_form' => 0, + 'acfe_meta' => '', + 'acfe_note' => '', +)); + } \ No newline at end of file diff --git a/source/fields/php/local-domains.php b/source/fields/php/local-domains.php new file mode 100644 index 0000000..d0ed422 --- /dev/null +++ b/source/fields/php/local-domains.php @@ -0,0 +1,78 @@ + 'group_6718e7ca78c94', + 'title' => __('Local Domain Settings', 'api-event-manager'), + 'fields' => array( + 0 => array( + 'key' => 'field_6718e7d0b54f7', + 'label' => __('Local domains', 'api-event-manager'), + 'name' => 'broken_links_local_domains', + 'aria-label' => '', + 'type' => 'repeater', + 'instructions' => __('Add domains in this list, that should not be checked.', 'api-event-manager'), + 'required' => 0, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'acfe_repeater_stylised_button' => 1, + 'layout' => 'table', + 'pagination' => 0, + 'min' => 0, + 'max' => 0, + 'collapsed' => '', + 'button_label' => __('Lägg till rad', 'api-event-manager'), + 'rows_per_page' => 20, + 'sub_fields' => array( + 0 => array( + 'key' => 'field_6718e860b54f9', + 'label' => __('Domain', 'api-event-manager'), + 'name' => 'domain', + 'aria-label' => '', + 'type' => 'url', + 'instructions' => __('eg. https://domain.com or https://subdomain.domain.com', 'api-event-manager'), + 'required' => 0, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'placeholder' => '', + 'parent_repeater' => 'field_6718e7d0b54f7', + ), + ), + ), + ), + 'location' => array( + 0 => array( + 0 => array( + 'param' => 'options_page', + 'operator' => '==', + 'value' => 'broken-links-settings', + ), + ), + ), + 'menu_order' => 0, + 'position' => 'normal', + 'style' => 'default', + 'label_placement' => 'left', + 'instruction_placement' => 'label', + 'hide_on_screen' => '', + 'active' => true, + 'description' => '', + 'show_in_rest' => 0, + 'acfe_display_title' => '', + 'acfe_autosync' => array( + 0 => 'json', + ), + 'acfe_form' => 0, + 'acfe_meta' => '', + 'acfe_note' => '', +)); + } \ No newline at end of file diff --git a/source/js/broken-link-detector.js b/source/js/broken-link-detector.js deleted file mode 100644 index 6f4274f..0000000 --- a/source/js/broken-link-detector.js +++ /dev/null @@ -1 +0,0 @@ -var BrokenLinkDetector = {}; diff --git a/source/js/context-detector.ts b/source/js/context-detector.ts new file mode 100644 index 0000000..24af520 --- /dev/null +++ b/source/js/context-detector.ts @@ -0,0 +1,120 @@ +class ClientTypeChecker { + private timedOut: boolean = false; + private img: HTMLImageElement = new Image(); + private timer: number | null = null; + + constructor( + private config: brokenLinkContextDetectionData + ) { + this.initializeCheck(); + } + + // Initialize the client type check + private initializeCheck(): void { + this.setTimer(); + this.loadImage(); + } + + // Set a timeout for image loading + private setTimer(): void { + this.timer = window.setTimeout(() => { + this.timedOut = true; + this.cancelImageLoad(); + this.setExternalClient(); + }, this.config.checkTimeout); + } + + // Load the image and handle success or failure + private loadImage(): void { + this.img.onload = this.handleImageLoad; + this.img.onerror = this.handleImageError; + this.img.src = this.config.checkUrl; + } + + // Handle successful image load (internal client) + private handleImageLoad = (): void => { + if (!this.timedOut) { + this.clearTimer(); + this.setInternalClient(); + } + } + + // Handle image load error (external client) + private handleImageError = (): void => { + if (!this.timedOut) { + this.clearTimer(); + this.setExternalClient(); + } + } + + // Clear the timer if image loads or errors before timeout + private clearTimer(): void { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + // Cancel image loading on timeout + private cancelImageLoad(): void { + this.img.src = ''; // Cancel image loading + } + + // Mark as internal client + private setInternalClient(): void { + console.log('Internal client'); + document.body.classList.add(this.config.successClass); + } + + // Mark as external client + private setExternalClient(): void { + document.body.classList.add(this.config.failedClass); + this.applyDomainRestrictions(); + } + + // Log messages only if DevTools is open + private log(message: string): void { + console.log(message); + } + + // Apply domain restrictions to domains in the domain list + private applyDomainRestrictions(): void { + this.config.domains.forEach(domain => { + const elements = document.querySelectorAll(`a[href*="${domain}"]`); + console.log(elements); + elements.forEach(element => { + element.classList.add('broken-link-detector-link-is-unavabile'); + element.setAttribute("data-tooltip", this.config.tooltip); + element.addEventListener("click", (event) => { + event.preventDefault(); + }); + }); + }); + } +} + +// Interfaces +interface brokenLinkContextDetectionData { + isEnabled: string; // Assuming '1' or '0' as string, change to `boolean` if converted + checkUrl: string; + checkTimeout: number; // or number if it’s parsed as a number + domains: string[]; + tooltip: string; + successClass: string; + failedClass: string; +} + +declare global { + interface Window { + brokenLinkContextDetectionData?: brokenLinkContextDetectionData; + } +} + +// @ts-ignore Function to initialize client type checker +export function initializeClientTypeChecker(brokenLinkContextDetectionData): void { + document.addEventListener("DOMContentLoaded", () => { + new ClientTypeChecker(brokenLinkContextDetectionData); + }); +} +// @ts-ignore +initializeClientTypeChecker(brokenLinkContextDetectionData); \ No newline at end of file diff --git a/source/js/editor-highlight.js b/source/js/editor-highlight.js new file mode 100644 index 0000000..1460c9c --- /dev/null +++ b/source/js/editor-highlight.js @@ -0,0 +1,22 @@ +(function waitForTinyMCE(retries = 100) { + if (typeof tinymce !== 'undefined') { + function applyBrokenLinkHighlight(editor) { + let styles = ''; + brokenLinkEditorHighlightData.links.forEach(function(item) { + styles += `a[data-mce-href*="${item}"] {text-decoration: underline wavy #f00 !important; cursor: not-allowed; pointer-events: none;}`; + }); + const styleElement = editor.dom.create('style', { id: 'broken-link-styles' }, styles); + editor.getDoc().head.appendChild(styleElement); + } + + window.onload = function() { + tinymce.editors.forEach(function(editor) { + applyBrokenLinkHighlight(editor); + }); + }; + } else if (retries > 0) { + setTimeout(() => waitForTinyMCE(retries - 1), 350); + } else { + console.warn("TinyMCE failed to load within the specified retries."); + } +})(); \ No newline at end of file diff --git a/source/mce/mce-broken-link-detector.js b/source/mce/mce-broken-link-detector.js deleted file mode 100644 index 80c3ce4..0000000 --- a/source/mce/mce-broken-link-detector.js +++ /dev/null @@ -1,19 +0,0 @@ -(function() { - tinymce.PluginManager.add('brokenlinksdetector', function(editor, url) { - var selectors = 'a[href="#"]'; - var selectors_before = 'a[href="#"]::before'; - var selectors_after = 'a[href="#"]::after'; - - // Ajax to get broken links for this post - jQuery.each(broken_links, function (index, item) { - selectors = selectors + ', a[data-mce-href="' + item + '"]'; - selectors_before = selectors_before + ', a[data-mce-href="' + item + '"]::before'; - selectors_after = selectors_after + ', a[data-mce-href="' + item + '"]::after'; - }); - - editor.contentStyles.push(selectors + " { color: #ff0000; text-decoration-style: wavy; }"); - editor.contentStyles.push(selectors_before + ' { display: inline-block; margin-right: 3px; content: ""; font-family: dashicons; position: relative; top: 4px; text-dociration: none; }'); - editor.contentStyles.push(selectors_after + ' { display: inline-block; margin-left: 3px; content: "Broken link"; font-family: arial; font-size: 12px; background-color: #ff0000; color: #fff; border-radius: 3px; padding: 0 4px; position: relative; top: -1px; }'); - }); -})(); - diff --git a/source/php/Admin/Settings/SanitizeLocalDomainSetting.php b/source/php/Admin/Settings/SanitizeLocalDomainSetting.php new file mode 100644 index 0000000..4f29393 --- /dev/null +++ b/source/php/Admin/Settings/SanitizeLocalDomainSetting.php @@ -0,0 +1,43 @@ +wpService->addFilter( + 'acf/update_value/key=field_6718e860b54f9', + [$this, 'sanitizeDomainValue'], + 10, + 4 + ); + } + + /** + * Sanitize the domain value + * + * @param string $value + * @param int $postId + * @param array $field + * @param string $original + * + * @return string + */ + public function sanitizeDomainValue($value, $postId, $field, $original): string + { + $parsedUrl = parse_url($value); + if ($parsedUrl === false || empty($parsedUrl['host'])) { + return ''; + } + return $parsedUrl['scheme'] . "://" . $parsedUrl['host']; + } + +} \ No newline at end of file diff --git a/source/php/Admin/Settings/SettingsPage.php b/source/php/Admin/Settings/SettingsPage.php new file mode 100644 index 0000000..ec986e1 --- /dev/null +++ b/source/php/Admin/Settings/SettingsPage.php @@ -0,0 +1,72 @@ + $filters + */ + public function __construct( + private AddAction&__ $wpService, + private AddOptionsPage $acfService, + private iterable $additionalHooks // Inject iterable of Hookable objects + ) {} + + /** + * Add hooks + * + * @return void + */ + public function addHooks(): void + { + $this->wpService->addAction('acf/init', [$this, 'registerSettingsPage']); + $this->registerAdditionalHooks(); + } + + /** + * Register additional hooks provided in $additionalHooks + * + * @return void + */ + private function registerAdditionalHooks(): void + { + foreach ($this->additionalHooks as $hook) { + if (!$hook instanceof Hookable) { + throw new \InvalidArgumentException( + sprintf('Expected instance of Hookable, got %s', get_debug_type($hook)) + ); + } + $hook->addHooks(); + } + } + + /** + * Register the settings page + * + * @return void + */ + public function registerSettingsPage(): void + { + $this->acfService->addOptionsPage(array( + 'menu_slug' => 'broken-links-settings', + 'page_title' => $this->wpService->__('Broken Links Settings', 'broken-link-detector'), + 'active' => true, + 'menu_title' => $this->wpService->__('Broken Links Settings', 'broken-link-detector'), + 'capability' => 'administrator', + 'parent_slug' => 'options-general.php', + 'position' => '', + 'icon_url' => '', + 'redirect' => true, + 'post_id' => 'options', + 'autoload' => true, + 'update_button' => $this->wpService->__('Update', 'broken-link-detector'), + 'updated_message' => $this->wpService->__('Settings updated', 'broken-link-detector'), + )); + } +} \ No newline at end of file diff --git a/source/php/Admin/Summary/OptionsPage.php b/source/php/Admin/Summary/OptionsPage.php new file mode 100644 index 0000000..47f8d19 --- /dev/null +++ b/source/php/Admin/Summary/OptionsPage.php @@ -0,0 +1,58 @@ +wpService->addAction('admin_menu', [$this, 'registerSummaryPage']); + } + + public function registerSummaryPage(): void + { + // Add the management page in the WordPress admin + $this->wpService->addManagementPage( + $this->wpService->__('Broken Links Report', 'broken-link-detector'), + $this->wpService->__('Broken Links Report', 'broken-link-detector'), + 'edit_pages', + 'broken-links-report', + [$this, 'renderSummaryPage'] + ); + } + + public function renderSummaryPage(): void + { + echo '
'; + echo '

' . esc_html__('Broken Links Report', 'broken-link-detector') . '

'; + echo '

' . esc_html__('Here is a summary of broken links found in your content.', 'broken-link-detector') . '

'; + + echo '
'; + echo '
'; + echo ''; + + // Initialize and display the table + $table = new Table($this->wpService, $this->db, $this->config); + $table->prepare_items(); + $table->display(); + + echo '
'; + echo '
'; + } +} diff --git a/source/php/Admin/Summary/Table.php b/source/php/Admin/Summary/Table.php new file mode 100644 index 0000000..f9e65da --- /dev/null +++ b/source/php/Admin/Summary/Table.php @@ -0,0 +1,126 @@ + __('Broken Link', 'broken-link-detector'), + 'plural' => __('Broken Links', 'broken-link-detector'), + 'ajax' => false, + ]); + } + + public function getBrokenLinks(): array + { + // Filtering by http_code if selected + $httpCodeFilter = isset($_REQUEST['http_code_filter']) ? absint($_REQUEST['http_code_filter']) : null; + + // Sorting parameters + $orderby = !empty($_REQUEST['orderby']) + ? sanitize_sql_orderby($_REQUEST['orderby']) + : 'time'; + $order = !empty($_REQUEST['order']) && in_array(strtoupper($_REQUEST['order']), ['ASC', 'DESC']) + ? strtoupper($_REQUEST['order']) + : 'ASC'; + + //Build the query + $query = $this->db->getInstance()->prepare( + "SELECT * FROM {$this->db->getTableName()} WHERE http_code != %d" + , 200); + + //Add filter if selected + if ($httpCodeFilter) { + $query .= $this->db->getInstance()->prepare(" AND http_code = %d", $httpCodeFilter); + } + + //Add sorting + $query .= " ORDER BY {$orderby} {$order}"; + + return $this->db->getInstance()->get_results($query); + } + + private function retriveHttpCodes(): array + { + $result = $this->db->getInstance()->get_col( + "SELECT DISTINCT http_code FROM {$this->db->getTableName()}" + ); + return array_map('absint', $result ?? []); + } + + public function prepare_items(): void + { + $columns = $this->get_columns(); + $hidden = []; + $sortable = $this->get_sortable_columns(); + + $this->_column_headers = [$columns, $hidden, $sortable]; + $this->items = $this->getBrokenLinks(); + } + + public function get_columns(): array + { + return [ + 'post_id' => __('Post', 'broken-link-detector'), + 'url' => __('URL', 'broken-link-detector'), + 'http_code' => __('HTTP Code', 'broken-link-detector'), + 'time' => __('Last Checked', 'broken-link-detector'), + ]; + } + + public function get_sortable_columns(): array + { + return [ + 'http_code' => ['http_code', false], + 'time' => ['time', false], + ]; + } + + protected function column_default($item, $column_name) + { + if ($column_name === 'post_id') { + // Retrieve the permalink for the post ID and display as clickable link + $postId = $item->post_id; + if ($postId) { + $permalink = get_permalink($postId); + $title = get_the_title($postId); + return sprintf( + '%s', + esc_url($permalink), + esc_html($title ?: __('View Post', 'broken-link-detector')) + ); + } + return __('N/A', 'broken-link-detector'); + } + + if ($column_name === 'http_code') { + return $this->wpService->getStatusHeaderDesc($item->$column_name) . " (" . $item->$column_name . ")"; + } + + return $item->$column_name ?? ''; + } + + protected function extra_tablenav($which): void + { + if ($which === 'top') { + // Dropdown filter for HTTP Code + $selectedCode = isset($_REQUEST['http_code_filter']) ? absint($_REQUEST['http_code_filter']) : ''; + echo '
'; + echo ''; + submit_button(__('Filter'), 'button', '', false); + echo '
'; + } + } +} \ No newline at end of file diff --git a/source/php/App.php b/source/php/App.php index ac30548..fc2c9d1 100644 --- a/source/php/App.php +++ b/source/php/App.php @@ -2,260 +2,221 @@ namespace BrokenLinkDetector; -class App -{ - public static $dbTable = 'broken_links_detector'; - public static $installChecked = false; - public static $wpdb = null; +/* Services */ +use WpService\WpService; +use AcfService\AcfService; - public static $externalDetector = false; - - public function __construct() - { - global $wpdb; - self::$wpdb = $wpdb; +/* Config & Features */ +use BrokenLinkDetector\Config\Config; +use BrokenLinkDetector\Config\Feature; - self::$dbTable = $wpdb->prefix . self::$dbTable; +/* Helpers */ +use BrokenLinkDetector\Database\Database; - add_action('admin_menu', array($this, 'addListTablePage')); +/* Link updater */ +use BrokenLinkDetector\LinkUpdater\LinkUpdater; - add_action('admin_enqueue_scripts', array($this, 'enqueueStyles')); - add_action('admin_enqueue_scripts', array($this, 'enqueueScripts')); +/* Admin functions */ +use BrokenLinkDetector\Admin\Editor; +use BrokenLinkDetector\Admin\Settings\SanitizeLocalDomainSetting; - add_filter('wp_insert_post_data', array($this, 'checkSavedPost'), 10, 2); +/* Link Registry */ +use BrokenLinkDetector\BrokenLinkRegistry\Registry\ManageRegistry; +use BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLink; +use BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostContent; +use BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostMeta; - add_action('wp', array($this, 'postTypeColumns')); - add_action('before_delete_post', array($this, 'deleteBrokenLinks')); - add_action('post_submitbox_misc_actions', array($this, 'rescanPost'), 100); +/* Cli commands */ +use BrokenLinkDetector\Cli\CommandRunner; +use BrokenLinkDetector\Cli\FindLinks; +use BrokenLinkDetector\Cli\ClassifyLinks; - $this->brokenLinksColumnSorting(); +class App +{ + public static $dbTable = 'broken_links_detector'; + public static $installChecked = false; + public static $wpdb = null; - self::$externalDetector = new \BrokenLinkDetector\ExternalDetector(); - new \BrokenLinkDetector\Editor(); - } + public static $externalDetector = false; - public static function checkInstall() + public function __construct( + WpService $wpService, + AcfService $acfService, + Database $db, + ManageRegistry $registry, + Config $config, + CommandRunner $cliRunner + ) { - if (self::$installChecked) { - return; + + /** + * Register activation and deactivation hooks + */ + if (Feature::factory('installer')->isEnabled(1)) { + $registerActivation = new \BrokenLinkDetector\Installer( + $wpService, + $config, + $db + ); + $registerActivation->addHooks(); } - $tableName = self::$dbTable; - - //Update to 1.0.1 - if(get_site_option('broken-links-detector-db-version') == "1.0.0") { + /** + * Load text domain + */ + if (Feature::factory('language')->isEnabled(1)) { + $loadTextDomain = new \BrokenLinkDetector\TextDomain( + $wpService, + $config + ); + $loadTextDomain->addHooks(); + } - $charsetCollation = self::$wpdb->get_charset_collate(); - $tableName = self::$dbTable; + /** + * Settings page to configure the plugin + * + * @capability: administrators + */ + if (Feature::factory('admin_settings')->isEnabled(1)) { + $registerAdminSettingsPage = new \BrokenLinkDetector\Admin\Settings\SettingsPage( + $wpService, + $acfService, + [ + new SanitizeLocalDomainSetting($wpService, $acfService) + ] + ); + $registerAdminSettingsPage->addHooks(); + } - $sql = "ALTER TABLE $tableName - ADD time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"; - - require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); - dbDelta($sql); + /** + * Summary page to view a list of broken links + * + * @capability: editors + */ + if (Feature::factory('admin_summary')->isEnabled(1)) { + $registerAdminSettingsPage = new \BrokenLinkDetector\Admin\Summary\OptionsPage( + $wpService, + $db, + $config + ); + $registerAdminSettingsPage->addHooks(); + } - update_option('broken-links-detector-db-version', '1.0.1'); + /** + * Add MCE editor interface to highlight broken links + */ + if (Feature::factory('admin_highlight_links')->isEnabled(1)) { + $editorHighlightAsset = new \BrokenLinkDetector\Asset\EditorHighlight( + $wpService, + $config, + $registry + ); + $editorHighlightAsset->addHooks(); } - if(get_site_option('broken-links-detector-db-version')) { - return true; + /** + * Loads editor and options fields + */ + if (Feature::factory('field_loader')->isEnabled(1)) { + $fieldLoader = new \BrokenLinkDetector\Fields\AcfExportManager\RegisterFieldConfiguration( + $wpService, + $config->getPluginFieldsPath() + ); + $fieldLoader->addHooks(); } - //Install - if(!self::$wpdb->get_var("SHOW TABLES LIKE '$tableName'") == $tableName) { - self::install(); + /** + * Register internal link updater + */ + if (Feature::factory('fix_internal_links')->isEnabled(1)) { + $internalLinkUpdater = new \BrokenLinkDetector\LinkUpdater\LinkUpdater( + $wpService, + $config, + $db, + $registry + ); + $internalLinkUpdater->addHooks(); } - } - public function rescanPost() - { - echo '
- - ' . __('The rescan will be executed for this post only. The scan will execute direcly after the save is completed and may take a few minutes to complete.', 'broken-links-detector') . ' -
'; - } + /** + * Register link registry maintner + * This will delete links in the registry connected to posts. + * It will also add links to the registry when a post is saved as new unclassified links. + * A reclasification of the links will need to be done to update the status of the links. + */ + if (Feature::factory('maintain_link_registry')->isEnabled(1)) { + $findLinksOnSavePost = new \BrokenLinkDetector\Hooks\MaintainLinkRegistryOnSavePost( + $wpService, + $config, + $db, + $registry + ); + $findLinksOnSavePost->addHooks(); + } - /** - * Sort post if sorting on broken-links column - * @return void - */ - public function brokenLinksColumnSorting() - { - add_filter('posts_fields', function ($fields, $query) { - if ($query->get('orderby') !== 'broken-links') { - return $fields; + /** + * Cli commands + */ + if(Feature::factory('cli')->isEnabled(1)) { + + //Commands for database management + if (Feature::factory('cli_installer')->isEnabled(1)) { + $installer = new \BrokenLinkDetector\Installer( + $wpService, + $config, + $db + ); + + $cliRunner->addCommand(new \BrokenLinkDetector\Cli\Database( + $wpService, + $config, + $installer + ))->registerWithWPCLI(); } - global $wpdb; - - $fields .= ", ( - SELECT COUNT(*) - FROM " . self::$dbTable . " - WHERE post_id = {$wpdb->posts}.ID - ) AS broken_links_count"; - - return $fields; - }, 10, 2); - - add_filter('posts_orderby', function ($orderby, $query) { - if ($query->get('orderby') !== 'broken-links') { - return $orderby; + // Commands for finding and registering links + if(Feature::factory('cli_link_finder')->isEnabled(1)) { + $cliRunner->addCommand(new \BrokenLinkDetector\Cli\FindLinks( + $wpService, + $config, + $db, + $registry + ))->registerWithWPCLI(); } - - $orderby = "broken_links_count {$query->get('order')}, " . $orderby; - return $orderby; - }, 10, 2); - } - - /** - * Add broken links column to post type post list table - * @return void - */ - public function postTypeColumns() - { - $postTypes = get_post_types(); - - foreach ($postTypes as $postType) { - add_filter('manage_' . $postType . '_posts_columns', function ($columns) { - broken_link_detector_array_splice_assoc($columns, -1, 0, array( - 'broken-links' => __('Broken links', 'broken-links-detector') - )); - - return $columns; - }, 50); - - add_filter('manage_edit-' . $postType . '_sortable_columns', function ($columns) { - $columns['broken-links'] = 'broken-links'; - return $columns; - }, 50); - - add_action('manage_' . $postType . '_posts_custom_column', function ($column, $postId) { - if ($column !== 'broken-links') { - return; - } - - $links = \BrokenLinkDetector\ListTable::getBrokenLinksCount($postId); - - if ($links > 0) { - echo '' . $links . ''; - } else { - echo ''; - } - }, 20, 2); - } - } - - /** - * Adds the list table page of broken links - */ - public function addListTablePage() - { - add_submenu_page( - 'options-general.php', - 'Broken links', - 'Broken links', - 'edit_posts', - 'broken-links-detector', - function () { - \BrokenLinkDetector\App::checkInstall(); - - $listTable = new \BrokenLinkDetector\ListTable(); - - $offset = get_option('gmt_offset'); - - if ($offset > -1) { - $offset = '+' . $offset; - } else { - $offset = '-' . (1 * abs($offset)); - } - - $nextRun = date('Y-m-d H:i', strtotime($offset . ' hours', wp_next_scheduled('broken-links-detector-external'))); - - include BROKENLINKDETECTOR_TEMPLATE_PATH . 'list-table.php'; + + //Commands for classifying links + if(Feature::factory('cli_link_classifier')->isEnabled(1)) { + $cliRunner->addCommand(new \BrokenLinkDetector\Cli\ClassifyLinks( + $wpService, + $config, + $db, + $registry + ))->registerWithWPCLI(); } - ); - } - - /** - * Setsup the database table on plugin activation (hooked in App.php) - * @return void - */ - public static function install() - { - $charsetCollation = self::$wpdb->get_charset_collate(); - $tableName = self::$dbTable; - if (!empty(get_site_option('broken-links-detector-db-version')) && self::$wpdb->get_var("SHOW TABLES LIKE '$tableName'") == $tableName) { - return; } - $sql = "CREATE TABLE $tableName ( - id bigint(20) NOT NULL AUTO_INCREMENT, - post_id bigint(20) DEFAULT NULL, - url varchar(255) DEFAULT '' NOT NULL, - time timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY id (id) - ) $charsetCollation;"; - - require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); - dbDelta($sql); - - update_option('broken-links-detector-db-version', '1.0.1'); - } - - /** - * Drops the database table on plugin deactivation (hooked in App.php) - * @return void - */ - public static function uninstall() - { - $tableName = self::$dbTable; - $sql = 'DROP TABLE ' . $tableName; - - self::$wpdb->query($sql); - - delete_option('broken-links-detector-db-version'); - } - - /** - * Checks if a saved posts permalink is changed and updates permalinks throughout the site - * @param array $data Post data - * @param array $postarr $_POST data - * @return array Post data (do not change) - */ - public function checkSavedPost($data, $postarr) - { - remove_action('wp_insert_post_data', array($this, 'checkSavedPost'), 10, 2); - - $detector = new \BrokenLinkDetector\InternalDetector($data, $postarr); - return $data; - } - - /** - * Remove broken links when deleting a page - * @param int $postId The post id that is being deleted - */ - public function deleteBrokenLinks($postId) - { - global $wpdb; - $tableName = self::$dbTable; - $wpdb->delete($tableName, array('post_id' => $postId), array('%d')); - } + /** + * Context detection frontend + */ + if (Feature::factory('context_detection')->isEnabled(1) && $config->isContextCheckEnabled()) { + $contextDetectionAsset = new \BrokenLinkDetector\Asset\ContextDetection( + $wpService, + $config + ); + $contextDetectionAsset->addHooks(); + } - /** - * Enqueue required style - * @return void - */ - public function enqueueStyles() - { - wp_enqueue_style('broken-links-detector', BROKENLINKDETECTOR_URL . '/dist/css/broken-link-detector.min.css', '', '1.0.0'); - } + /** + * General frontend styles + */ + if (Feature::factory('frontend_styles')->isEnabled(1)) { + $contextDetectionAsset = new \BrokenLinkDetector\Asset\FrontendStyles( + $wpService, + $config + ); + $contextDetectionAsset->addHooks(); + } - /** - * Enqueue required scripts - * @return void - */ - public function enqueueScripts() - { } } diff --git a/source/php/Asset/AssetInterface.php b/source/php/Asset/AssetInterface.php new file mode 100644 index 0000000..17980ea --- /dev/null +++ b/source/php/Asset/AssetInterface.php @@ -0,0 +1,18 @@ +config = $config; + + if (!is_null($registry)) { + $this->registry = $registry; + } + } + + public function addHooks(): void + { + if(!in_array($this->getHook(), ['wp_enqueue_scripts', 'admin_enqueue_scripts', 'login_enqueue_scripts', 'mce_external_plugins'])) { + throw new \Exception('Invalid hook enqueued in Enqueue class. Must be either "mce_external_plugins", "wp_enqueue_scripts", "admin_enqueue_scripts" or "login_enqueue_scripts"'); + } + $this->wpService->addAction($this->getHook(), [$this, 'register'], 10); + $this->wpService->addAction($this->getHook(), [$this, 'enqueue'], 20); + } + + private function getType($filename): string + { + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if (in_array($extension, ["css", "js"])) { + return $extension; + } + + throw new \Exception('Invalid type enqueued in Enqueue class. Must be either ".js" or ".css"'); + } + + /** + * Register the script or style + * + * @return void + * @thows \Exception + */ + public function register(): void + { + $filename = $this->getFilename(); + + if ($this->getType($filename) === 'js') { + $this->wpService->wpRegisterScript( + $this->getHandle(), + $this->getFilename(), + $this->getDependencies(), + false, + ['in_footer' => true] + ); + + if (!empty($this->getLocalizeData())) { + $this->wpService->wpLocalizeScript( + $this->getHandle(), + $this->camelCaseObjectName($this->getHandle() . '-data'), + $this->getLocalizeData() + ); + } + } + + if ($this->getType($filename) === 'css') { + $this->wpService->wpRegisterStyle( + $this->getHandle(), + $this->getFilename(), + $this->getDependencies(), + ); + + if($this->getLocalizeData() !== null) { + throw new \Exception('Localize data is not supported for styles'); + } + } + } + + /** + * Enqueue the script or style + * + * @return void + */ + + public function enqueue(): void + { + $filename = $this->getFilename(); + + if ($this->getType($filename) === 'css') { + $this->wpService->wpEnqueueStyle($this->getHandle()); + } + if ($this->getType($filename) === 'js') { + $this->wpService->wpEnqueueScript( + $this->getHandle() + ); + } + } + + /** + * Get the object name in camel case + * + * @param string $objectName + * @return string + */ + private function camelCaseObjectName($objectName): string + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $objectName)))); + } +} \ No newline at end of file diff --git a/source/php/Asset/ContextDetection.php b/source/php/Asset/ContextDetection.php new file mode 100644 index 0000000..251203a --- /dev/null +++ b/source/php/Asset/ContextDetection.php @@ -0,0 +1,41 @@ + $this->config->isContextCheckEnabled(), + 'checkUrl' => $this->config->getContextCheckUrl(), + 'checkTimeout' => $this->config->getContextCheckTimeout(), + 'domains' => $this->config->getContextCheckDomainsToDisable(), + 'tooltip' => $this->config->getContextCheckTooltipText(), + 'successClass' => $this->config->getContextCheckSuccessClass(), + 'failedClass' => $this->config->getContextCheckFailedClass(), + ]; + } +} \ No newline at end of file diff --git a/source/php/Asset/EditorHighlight.php b/source/php/Asset/EditorHighlight.php new file mode 100644 index 0000000..dd5c716 --- /dev/null +++ b/source/php/Asset/EditorHighlight.php @@ -0,0 +1,53 @@ +registry || !$this->getPostid()) { + return []; + } + + $links = $this->registry->getBrokenLinksByPostId( + $this->getPostid() + ); + $links = array_column($links, 'url'); + + return [ + 'links' => $links, + ]; + } + + /** + * Get the post id + */ + private function getPostid(): ?int + { + global $post; + return $post->ID ?? null; + } +} \ No newline at end of file diff --git a/source/php/Asset/FrontendStyles.php b/source/php/Asset/FrontendStyles.php new file mode 100644 index 0000000..a4d9b1b --- /dev/null +++ b/source/php/Asset/FrontendStyles.php @@ -0,0 +1,33 @@ +httpCode) && $this->shouldClassify($url)) { + + //Check if the URL is already classified in this run + if(array_key_exists($this->url, self::$statusCodeCache)) { + $this->httpCode = self::$statusCodeCache[$this->url]; + return; + } + + if($this->isInternal()) { + if($this->tryGetHttpCodeByPostStatus() === null) { + $this->tryGetHttpCodeByUrlResponse(); + } + } else { + $this->tryGetHttpCodeByUrlResponse(); + } + + //Store the http code in cache + if(!is_null($this->httpCode)) { + self::$statusCodeCache[$this->url] = $this->httpCode; + } + } + } + + /** + * Check if the URL is internal. + * + * @return bool + */ + public function isInternal(): bool + { + return $this->getSiteDomain() === $this->getUrlDomain(); + } + + /** + * Check if the URL is external. + * + * @return bool + */ + public function isExternal(): bool + { + return $this->getSiteDomain() !== $this->getUrlDomain(); + } + + /** + * Check if the URL is broken, by provided http codes. + */ + public function isBroken(): ?bool + { + $brokenCodes = $this->config->responseCodesConsideredBroken(); + if(in_array($this->httpCode, $brokenCodes)) { + return true; + } + return false; + } + + /** + * Get the http code of the URL. + */ + public function getHttpCode(): ?int + { + return $this->httpCode; + } + + /** + * Try to get the post status of the URL internally. + * + * @return int|null The http code if successful, otherwise null + */ + private function tryGetHttpCodeByPostStatus(): ?int + { + $post = $this->wpService->urlToPostId($this->url); + if($post && $this->wpService->getPostStatus($post) == 'publish') { + return $this->httpCode = 200; + } + return null; + } + + /** + * Try to get the DNS record of the domain. + * + * @return bool True if the DNS record was found, otherwise false + */ + private function tryGetDnsRecord(): bool + { + static $dnsCache = []; + + if (!$this->config->checkIfDnsRespondsBeforeProbingUrl() || !function_exists('dns_get_record')) { + return true; + } + + try { + $domain = $this->getUrlDomain(); + + if (isset($dnsCache[$domain])) { + return $dnsCache[$domain]; + } + + return $dnsCache[$domain] = (bool) dns_get_record($domain, DNS_A) || (bool) dns_get_record($domain, DNS_CNAME); + + } catch(\Exception $e) { + return true; + } + } + + /** + * Try to get the http code of the URL. + * + * @return int The http code, or placeholder 503 if not responding/unreachable + */ + private function tryGetHttpCodeByUrlResponse(): int + { + //Check if the URL is reachable + if(!$this->tryGetDnsRecord()) { + return $this->httpCode = 503; //Bad gateway, no better code available (not really a http error) + } + + $response = $this->wpService->wpRemoteGet($this->url, [ + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36', + "Sec-Fetch-Mode" => "navigate", + ], + 'headers_only' => true, + 'redirection' => $this->config->getMaxRedirects(), + 'timeout' => $this->config->getTimeout(), + ]); + + if(!$this->wpService->isWpError($response)) { + $this->httpCode = $this->wpService->wpRemoteRetrieveResponseCode( + $response + ); + return $this->httpCode; + } + + return $this->httpCode = 503; //Not responding + } + + /** + * Factory method to create a new instance if url classification + * + * @param string $url + * @param WpService $wpService + * @return Classify + */ + public static function factory(string $url, ?int $httpCode, WpService $wpService, Config $config): Classify { + return new self($url, $httpCode, $wpService, $config); + } + + /** + * Check if the URL should be classified. + * + * @param string $url + * @return bool + */ + private function shouldClassify(string $url): bool { + $doNotClassifyDomains = $this->config->getDomainsThatShouldNotBeChecked(); + if(in_array($this->getHostName($url), $doNotClassifyDomains)) { + return false; + } + return true; + } + + /** + * Get the domain of the URL. + * + * @return string + */ + private function getUrlDomain(): string + { + return $this->getHostName($this->url); + } + + /** + * Get the domain of the site. + * + * @return string + */ + private function getSiteDomain(): string + { + static $siteUrl; + if(is_null($siteUrl)) { + $siteUrl = $this->wpService->siteUrl(); + } + return $this->getHostName($siteUrl); + } + + /** + * Extract the hostname from a URL. + * + * @param string $url + */ + private function getHostName(string $url): string + { + return parse_url($url, PHP_URL_HOST); + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.e2e.php b/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.e2e.php new file mode 100644 index 0000000..1369ae7 --- /dev/null +++ b/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.e2e.php @@ -0,0 +1,35 @@ +assertEquals(200, $classified->getHttpCode()); + $this->assertEquals(true, $classified->isExternal()); + $this->assertEquals(false, $classified->isBroken()); + } + + public function externalSuccessfullUrlProvider():array { + return [ + ['http://scb.se/'], + ['https://www.scb.se/'], + ['https://google.com/'], + ['https://www.naturvardsverket.se/'], + ['https://www.google.com/maps/d/u/0/?amp%3Bie=UTF8&%3Bll=56.034842%2C12.732468&%3Bmsa=0&%3Bmsid=208282319363100350016.00046a3e07a0c84e53bb7&%3Boe=UTF8&%3Bsource=embed&%3Bsp'], + ['https://www.facebook.com/tonicaorkestern'], + ]; + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.php b/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.php new file mode 100644 index 0000000..2b690de --- /dev/null +++ b/source/php/BrokenLinkRegistry/ClassifyLink/Classify.test.php @@ -0,0 +1,81 @@ + 'https://example.com/old-permalink/', + 'isWpError' => false, + 'siteUrl' => 'https://example.com', + 'applyFilters' => function($filter, $value) { + return $value; + }, + 'wpRemoteGet' => function($url) { + if ($url === 'https://this-domain-does-not-exists.tdl') { + return [ + 'response' => [ + 'code' => 503 + ] + ]; + } + return [ + 'response' => [ + 'code' => 200 + ] + ]; + }, + 'wpRemoteRetrieveResponseCode' => function($response) { + return $response['response']['code']; + } + ]); + + $acfService = new FakeAcfService([ + 'get_field' => [ + 'broken_links_local_domains' => ['https://example.com'], + ] + ]); + + $config = new Config($wpService, $acfService, 'filterPrefix', 'pluginPath', 'pluginUrl'); + + $link = Link::createLink( + $input, + null, + 1, + $wpService, + $config + ); + $link->classify(); + + $result = $link->classification->isBroken(); + + // Assert + $this->assertEquals($expected, $result); + } + + /** + * Test urls. + */ + private function uriProvider() + { + return [ + ['https://scb.se/', false], + ['https://this-domain-does-not-exists.tdl', true], + ['https://google.com', false] + ]; + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/ClassifyLink/ClassifyInterface.php b/source/php/BrokenLinkRegistry/ClassifyLink/ClassifyInterface.php new file mode 100644 index 0000000..206b309 --- /dev/null +++ b/source/php/BrokenLinkRegistry/ClassifyLink/ClassifyInterface.php @@ -0,0 +1,15 @@ +findLinkResolvers = $findLink; + } + + public function addHooks(): void + { + if(empty($this->findLinkResolvers)) { + throw new \InvalidArgumentException('No find link resolvers provided'); + } + + foreach ($this->findLinkResolvers as $resolver) { + $this->wpService->addAction( + $resolver->getHookName(), + array($resolver, 'findLinks'), + $resolver->getHookPriority(), + $resolver->getHookAcceptedArgs() + ); + } + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostContent.php b/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostContent.php new file mode 100644 index 0000000..fe0fe7e --- /dev/null +++ b/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostContent.php @@ -0,0 +1,127 @@ +linkList)) { + $this->linkList = new LinkList(); + } + } + + public function getHookName(): string + { + return __NAMESPACE__ . __CLASS__; + } + + public function getHookPriority(): int + { + return 10; + } + + public function getHookAcceptedArgs(): int + { + return 1; + } + + /** + * Find all links in the given post content. + * + * @param string $postContent + */ + public function findLinks(?int $postId = null): LinkList + { + $query = $this->createQuery($postId); + + $postsContainingLinks = $this->db->getInstance()->get_results($query); + + if(!$this->wpService->isWpError($postsContainingLinks)) { + foreach ($postsContainingLinks as $postItem) { + + $extractedLinks = $this->extractLinksFromPostContent($postItem->post_content); + + foreach ($extractedLinks as $url) { + $this->linkList->addLink( + Link::createLink( + $url, + null, + $postItem->ID, + $this->wpService, + $this->config + ) + ); + } + } + } + + return $this->linkList; + } + + /** + * Extract all links from the given post content. + * + * @param string $postContent + * @return array + */ + private function extractLinksFromPostContent(string $postContent): array + { + $matches = []; + preg_match_all('/href="(https?:\/\/[^"]+)"/', $postContent, $matches); + + return $matches[1]; + } + + /** + * Create the SQL query to find all posts containing links. + * + * @return string + */ + public function createQuery(?int $postId = null): string { + // Init DB object + $db = $this->db->getInstance(); + + // Get configuration + $bannedPostTypesArray = $this->config->linkDetectBannedPostTypes(); + $allowedPostStatuses = $this->config->linkDetectAllowedPostStatuses(); + + // Prepare placeholders for each banned post type and allowed status + $placeholdersTypes = implode(',', array_fill(0, count($bannedPostTypesArray), '%s')); + $placeholdersStatuses = implode(',', array_fill(0, count($allowedPostStatuses), '%s')); + + // Start building the base SQL query + $query = ' + SELECT ID, post_content + FROM ' . $db->posts . ' + WHERE + post_content RLIKE "href=\\"https?:\\/\\/" + AND post_type NOT IN (' . $placeholdersTypes . ') + AND post_status IN (' . $placeholdersStatuses . ')'; + + // If a postId is provided, add the condition to the query + if ($postId !== null) { + $query .= ' AND ID = %d'; + } + + // Merge parameters for binding (banned post types, allowed post statuses, and optional postId) + $queryParams = array_merge($bannedPostTypesArray, $allowedPostStatuses); + if ($postId !== null) { + $queryParams[] = $postId; // Add postId to parameters if provided + } + + // Prepare the SQL statement + $query = $db->prepare($query, $queryParams); + + return $query; + } + +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostMeta.php b/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostMeta.php new file mode 100644 index 0000000..1bf50fb --- /dev/null +++ b/source/php/BrokenLinkRegistry/FindLink/FindLinkFromPostMeta.php @@ -0,0 +1,125 @@ +linkList)) { + $this->linkList = new LinkList(); + } + } + + public function getHookName(): string + { + return __NAMESPACE__ . __CLASS__; + } + + public function getHookPriority(): int + { + return 10; + } + + public function getHookAcceptedArgs(): int + { + return 1; + } + + /** + * Find all links in the given post meta content. + * + * @return LinkList + */ + public function findLinks(?int $postId = null): LinkList + { + $query = $this->createQuery($postId); + $metaContainingLinks = $this->db->getInstance()->get_results($query); + + if (!$this->wpService->isWpError($metaContainingLinks)) { + foreach ($metaContainingLinks as $metaItem) { + $extractedLinks = $this->extractLinksFromMetaContent($metaItem->meta_value); + + foreach ($extractedLinks as $url) { + $this->linkList->addLink( + Link::createLink( + $url, + null, + $metaItem->post_id, + $this->wpService, + $this->config + ) + ); + } + } + } + + return $this->linkList; + } + + /** + * Extract all links from the given post meta content. + * + * @param string $metaContent + * @return array + */ + private function extractLinksFromMetaContent(string $metaContent): array + { + $matches = []; + preg_match_all('/(https?:\/\/[^"\s]+)/', $metaContent, $matches); + return $matches[0]; + } + + /** + * Create the SQL query to find all post meta entries containing links. + * + * @return string + */ + public function createQuery(?int $postId = null): string + { + // Init DB object + $db = $this->db->getInstance(); + + // Get configuration + $bannedPostTypesArray = $this->config->linkDetectBannedPostTypes(); + $allowedPostStatuses = $this->config->linkDetectAllowedPostStatuses(); + + // Prepare placeholders for each banned post type and allowed status + $placeholdersTypes = implode(',', array_fill(0, count($bannedPostTypesArray), '%s')); + $placeholdersStatuses = implode(',', array_fill(0, count($allowedPostStatuses), '%s')); + + // Start building the base SQL query + $query = ' + SELECT post_id, meta_value + FROM ' . $db->postmeta . ' pm + INNER JOIN ' . $db->posts . ' p ON pm.post_id = p.ID + WHERE + meta_value RLIKE "(https?:\\/\\/[^\\s\"]+)" + AND p.post_type NOT IN (' . $placeholdersTypes . ') + AND p.post_status NOT IN (' . $placeholdersStatuses . ')'; + + // If post_id is provided, add the condition to the query + if ($postId !== null) { + $query .= ' AND pm.post_id = %d'; + } + + // Merge parameters for binding (banned post types, allowed post statuses, and optional post_id) + $queryParams = array_merge($bannedPostTypesArray, $allowedPostStatuses); + if ($postId !== null) { + $queryParams[] = $postId; // Add post_id to parameters if provided + } + + // Prepare the SQL statement + $query = $db->prepare($query, $queryParams); + + return $query; + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/FindLink/FindLinkInterface.php b/source/php/BrokenLinkRegistry/FindLink/FindLinkInterface.php new file mode 100644 index 0000000..4fa63a3 --- /dev/null +++ b/source/php/BrokenLinkRegistry/FindLink/FindLinkInterface.php @@ -0,0 +1,13 @@ +classification = Classify::factory( + $this->url, + $this->httpCode, + self::$wpService, + self::$config + ); + + if(!is_null($this->classification->getHttpCode())) { + $this->httpCode = $this->classification->getHttpCode(); + $this->isBroken = $this->classification->isBroken(); + } + + $this->isInternal = $this->classification->isInternal(); + $this->isExternal = $this->classification->isExternal(); + + return $this->classification; + } + + /** + * Factory method to create a new instance of a link. + * + * @param string $url The url of the link + * @param int $httpCode The http code of the link, if known, otherwise null + * @param int $postId The post id where the link was found + * @param WpService $wpService The wp service instance + * @param Config $config The config instance + * + * @return Link + */ + public static function createLink(string $url, ?int $httpCode, int $postId, WpService $wpService, Config $config): Link { + self::$wpService = $wpService; + self::$config = $config; + return new Link($url, $httpCode, $postId); + } +} + +/* + Usage: Add call to classify to run the classification logic + (is internal, external, broken by http request or internal post status) + + $link = Link::createLink('https://www.google.com', 200, 1, $wpService, $config)->classify(); + +*/ \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/Link/LinkInterface.php b/source/php/BrokenLinkRegistry/Link/LinkInterface.php new file mode 100644 index 0000000..4bc8a0d --- /dev/null +++ b/source/php/BrokenLinkRegistry/Link/LinkInterface.php @@ -0,0 +1,13 @@ +links[] = $link; + } + + /** + * Classify all links in the list. + * + * @return bool True if all links were classified, false if no links avabile to classify. + */ + public function classifyLinks(): bool + { + if (empty($this->links)) { + return false; + } + foreach ($this->links as &$link) { + $link->classify(); + } + return true; + } + + /** + * Get all links in the list. + * + * @return array + */ + public function getLinks(): array + { + return $this->links; + } + + /** + * Get the number of links in the list. + * + * @return int + */ + public function getLinkCount(): int + { + return count($this->links); + } + +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/LinkList/LinkListInterface.php b/source/php/BrokenLinkRegistry/LinkList/LinkListInterface.php new file mode 100644 index 0000000..3ad1688 --- /dev/null +++ b/source/php/BrokenLinkRegistry/LinkList/LinkListInterface.php @@ -0,0 +1,13 @@ +addLinkList($linkListOrLink); + return true; + } + if ($linkListOrLink instanceof Link) { + $this->addLink($linkListOrLink); + return true; + } + return false; + } + + /** + * Update broken links (if already in db, checked by hash) + * @param array $data + * @return bool + */ + public function update(LinkList|Link $linkListOrLink): bool + { + if ($linkListOrLink instanceof LinkList) { + $this->updateLinkList($linkListOrLink); + return true; + } + if ($linkListOrLink instanceof Link) { + $this->updateLink($linkListOrLink); + return true; + } + return false; + } + + /** + * Update a link in the registry + * + * @param Link $link + * + * @return void + */ + private function updateLink(Link $link): void + { + $httpCode = $link->classification?->getHttpCode() ?? null; + + $x = $this->db->getInstance()->update( + $this->db->getInstance()->prefix . $this->config->getTableName(), + array( + 'http_code' => $httpCode, + 'time' => current_time('mysql') // Set the current timestamp + ), + array( + 'url' => $this->normalizeUrl($link->url), + ), + array('%d', '%s'), + array('%s') + ); + + //Log results + if ($lastError = $this->db->getInstance()->last_error) { + Log::warning("Error updating link in registry: " . $lastError); + } + } + + /** + * Update a list of links in the registry + * + * @param LinkList $linkList + * + * @return void + */ + private function updateLinkList(LinkList $linkList): void + { + foreach ($linkList->getLinks() as $link) { + $this->updateLink($link); + } + } + + /** + * Add a link to the registry + * + * @param Link $link + * + * @return void + */ + private function addLink(Link $link): void + { + $uniqueHash = $this->hash($link->url, $link->postId); + $httpCode = $link->classification?->getHttpCode() ?? null; + + $this->db->getInstance()->insert( + $this->db->getInstance()->prefix . $this->config->getTableName(), + array( + 'post_id' => $link->postId, + 'url' => $this->normalizeUrl($link->url), + 'unique_hash' => $uniqueHash, + 'http_code' => $httpCode, + 'time' => current_time('mysql') // Set the current timestamp + ), + array('%d', '%s', '%s', '%d', '%s') + ); + + //Log results + if ($lastError = $this->db->getInstance()->last_error) { + Log::warning("Error adding link to registry: " . $lastError . " for link: " . $link->url . " with postID: " . $link->postId); + } + } + + /** + * Add a list of links to the registry + * + * @param LinkList $linkList + * + * @return void + */ + private function addLinkList(LinkList $linkList): void + { + foreach ($linkList->getLinks() as $link) { + $this->addLink($link); + } + } + + /** + * Delete broken links by post id (eg. when a post is removed) + * @param integer $postId + * @return void + */ + public function remove(int $postId): void + { + $this->db->getInstance()->delete( + $this->config->getTableName(), + array('post_id' => $postId), + array('%d') + ); + } + + /** + * Get all unclassified links + * @return array + */ + public function getLinksThatNeedsClassification($maxLimit = null, $random = true): array + { + $recheckInterval = $this->config->getRecheckInterval(); + + $results = $this->db->getInstance()->get_results( + $this->db->getInstance()->prepare(" + SELECT * FROM + " . $this->db->getInstance()->prefix . $this->config->getTableName() . " + WHERE http_code IS NULL + OR time < DATE_SUB(NOW(), INTERVAL %d MINUTE) + ORDER BY " . ($random ? "RAND()" : "time ASC") . " + LIMIT %d", + $recheckInterval, (is_int($maxLimit) ? $maxLimit : PHP_INT_MAX) + ) + ); + + return $results; + } + + /** + * Get broken links by post id + * @param integer $postId + * @return array + */ + public function getBrokenLinksByPostId(int $postId): array + { + $httpCodesConsideredBroken = $this->config->responseCodesConsideredBroken(); + return $this->db->getInstance()->get_results( + $this->db->getInstance()->prepare( + "SELECT * FROM " . $this->db->getInstance()->prefix . $this->config->getTableName() . " + WHERE post_id = %d + AND http_code IN (" . implode(',', $httpCodesConsideredBroken) . ")", + $postId + ) + ); + } + + /** + * Create a hash from a normalized url and post id. + * + * @param string $url + * @param integer $postId + * @return string + */ + public function hash($url, $postId): string + { + return md5($this->normalizeUrl($url) . (string) $postId); + } + + /** + * Normalize the URL to remove insignificant differences. + * + * @param string $url + * + * @return string The normalized URL + */ + private function normalizeUrl($url): string + { + $parsedUrl = parse_url($url); + + $scheme = isset($parsedUrl['scheme']) ? strtolower($parsedUrl['scheme']) : 'http'; + $host = isset($parsedUrl['host']) ? strtolower($parsedUrl['host']) : ''; + $path = isset($parsedUrl['path']) ? rtrim($parsedUrl['path'], '/') : ''; + $query = ''; + + if (isset($parsedUrl['query'])) { + parse_str($parsedUrl['query'], $queryParams); + ksort($queryParams); + $query = http_build_query($queryParams); + } + + $normalizedUrl = $scheme . '://' . $host . $path; + if ($query) { + $normalizedUrl .= '?' . $query; + } + + return $normalizedUrl; + } +} \ No newline at end of file diff --git a/source/php/BrokenLinkRegistry/Registry/ManageRegistryInterface.php b/source/php/BrokenLinkRegistry/Registry/ManageRegistryInterface.php new file mode 100644 index 0000000..2083f2c --- /dev/null +++ b/source/php/BrokenLinkRegistry/Registry/ManageRegistryInterface.php @@ -0,0 +1,14 @@ + 'Optional: Limit the number of links to classify.', + ]; + } + + public function getCommandHandler(): callable + { + return function (array $arguments, array $options) { + $limit = isset($arguments['limit']) ? ((int) $arguments['limit']) : null; + $unclassifiedLinks = $this->registry->getLinksThatNeedsClassification($limit); + $totalNumberOfLinksToClassify = count($unclassifiedLinks); + + Log::info("Starting link classification of {$totalNumberOfLinksToClassify} links..."); + + $progress = \WP_CLI\Utils\make_progress_bar("Working: ", $totalNumberOfLinksToClassify); + + foreach ($unclassifiedLinks as $link) { + $linkObject = Link::createLink($link->url, null, $link->id, $this->wpService, $this->config); + $linkObject->classify(); + + if ($linkObject->httpCode !== null) { + $this->registry->update($linkObject); + $this->addToSummary($linkObject->isInternal, $linkObject->isBroken, $linkObject->httpCode, $linkObject->url); + } + + $progress->tick(); + } + + // Finish the progress bar + $progress->finish(); + + // Final success log + Log::success("Link classification completed. Found {$this->brokenInternalLinks} broken internal link(s) and {$this->brokenExternalLinks} broken external link(s)."); + + // Output the summary table + $this->getSummary(); + }; + } + + /** + * Add a link to the summary table if it's broken + * + * @param bool $isInternal + * @param bool $isBroken + * @param int|null $httpCode + * @param string $url + * + * @return void + */ + private function addToSummary(bool $isInternal, bool $isBroken, ?int $httpCode, string $url): void { + if ($isBroken) { + $this->brokenLinksSummary[] = [ + 'url' => $url, + 'httpCode' => $httpCode, + 'internal' => ($isInternal ? 'Internal' : 'External') + ]; + } + + if ($isInternal && $isBroken) { + $this->brokenInternalLinks++; + } elseif (!$isInternal && $isBroken) { + $this->brokenExternalLinks++; + } + } + + /** + * Output the summary table of broken links + * + * @return void + */ + private function getSummary(): void { + \WP_CLI\Utils\format_items('table', $this->brokenLinksSummary, ['url', 'httpCode', 'internal']); + } +} \ No newline at end of file diff --git a/source/php/Cli/CommandCommons.php b/source/php/Cli/CommandCommons.php new file mode 100644 index 0000000..1399cf4 --- /dev/null +++ b/source/php/Cli/CommandCommons.php @@ -0,0 +1,41 @@ +getCommandName()}"; + + // Add arguments to usage section + foreach ($this->getCommandArguments() as $arg => $desc) { + $usage .= " [--{$arg}=<{$arg}>]"; + } + + $usage .= "\n\nOptions:\n"; + + // Add arguments with descriptions to options section + foreach ($this->getCommandArguments() as $arg => $desc) { + $usage .= "--{$arg}: {$desc}\n"; + } + + // Add options with descriptions to options section + foreach ($this->getCommandOptions() as $opt => $desc) { + $usage .= "--{$opt}: {$desc}\n"; + } + + return $usage; + } + + abstract public function getCommandName(): string; + + abstract public function getCommandOptions(): array; + + abstract public function getCommandArguments(): array; + +} \ No newline at end of file diff --git a/source/php/Cli/CommandInterface.php b/source/php/Cli/CommandInterface.php new file mode 100644 index 0000000..879444e --- /dev/null +++ b/source/php/Cli/CommandInterface.php @@ -0,0 +1,13 @@ +commands[$command->getCommandName()] = $command; + return $this; + } + + /** + * Get all registered commands. + */ + public function getCommands(): array + { + return $this->commands; + } + + /** + * Run a command with the given arguments and options. + */ + public function runCommand(string $commandName, array $arguments, array $options): void + { + if (!isset($this->commands[$commandName])) { + throw new \Exception("Command not found"); + } + + $command = $this->commands[$commandName]; + $handler = $command->getCommandHandler(); + $handler($arguments, $options); + } + + /** + * Register each command with WP-CLI. + */ + public function registerWithWPCLI(): bool + { + if (!defined('WP_CLI') || (defined('WP_CLI') && !WP_CLI )) { + return false; + } + + foreach ($this->commands as $commandName => $command) { + WP_CLI::add_command("{$this->config->getCommandNamespace()} {$commandName}", function ($options, $arguments) use ($command) { + $handler = $command->getCommandHandler(); + $handler($arguments, $options); + }, [ + 'shortdesc' => $command->getCommandDescription(), + 'synopsis' => $this->generateSynopsis($command) + ]); + } + + return true; + } + + /** + * Generate the synopsis for a command. + */ + private function generateSynopsis(CommandInterface $command): array + { + $synopsis = []; + foreach ($command->getCommandArguments() as $arg => $desc) { + $synopsis[] = [ + 'type' => 'assoc', + 'name' => $arg, + 'description' => $desc, + 'optional' => false, + ]; + } + foreach ($command->getCommandOptions() as $opt => $desc) { + $synopsis[] = [ + 'type' => 'assoc', + 'name' => $opt, + 'description' => $desc, + 'optional' => true, + ]; + } + return $synopsis; + } +} \ No newline at end of file diff --git a/source/php/Cli/Database.php b/source/php/Cli/Database.php new file mode 100644 index 0000000..d915fda --- /dev/null +++ b/source/php/Cli/Database.php @@ -0,0 +1,67 @@ + 'Do a database action. (install, uninstall, reinstall)' + ]; + } + + public function getCommandOptions(): array + { + return []; + } + + public function getCommandHandler(): callable + { + return function (array $arguments, array $options) { + + $action = $arguments['action'] ?? null; + + if(!in_array($action, ['install', 'uninstall', 'reinstall'])) { + Log::error("Invalid argument, action must be set to 'install', 'uninstall' or 'reinstall'."); + return; + } + + if($action == 'install') { + $this->installer->install(); + Log::success("Database installed."); + } + + if($action == 'uninstall') { + $this->installer->uninstall(); + Log::success("Database uninstalled."); + Log::warning("No database is installed: this will cause errors if the plugin is not reinstalled correctly."); + } + + if($action == 'reinstall') { + $this->installer->reinstall(true); + Log::success("Database reinstalled."); + } + + }; + } +} \ No newline at end of file diff --git a/source/php/Cli/FindLinks.php b/source/php/Cli/FindLinks.php new file mode 100644 index 0000000..d509932 --- /dev/null +++ b/source/php/Cli/FindLinks.php @@ -0,0 +1,114 @@ + 'Optional: Search for links in content, default true.', + 'meta' => 'Optional: Search for links in meta, default true.' + ]; + } + + public function getCommandHandler(): callable + { + return function (array $arguments, array $options) { + + // Default options + $defaultOptions = [ + 'content' => true, + 'meta' => true + ]; + + //Set from command options + $options = array_merge($defaultOptions, $options); + + // Filter + $searchIn = function(array $options): array { + return array_keys(array_filter($options, fn($value) => $value === true)); + }; + $searchIn = $searchIn($options); + $searchIn = implode(",", $searchIn); + + if (empty(array_filter($options))) { + echo "No search options provided. Please provide at least one search option.\n"; + return; + } + + Log::log("Starting link check in: {$searchIn}..."); + + if(array_key_exists('content', $options)) { + $this->findLinksInContent(); + } + + if(array_key_exists('meta', $options)) { + $this->findLinksInMeta(); + } + }; + } + + private function findLinksInContent(): void + { + Log::log("Finding links in content..."); + + $findLinkFromPostContent = new \BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostContent( + $this->wpService, + $this->config, + $this->db + ); + + $foundLinks = $findLinkFromPostContent->findLinks(); + + Log::log("Found " . $foundLinks->getLinkCount() . " links in content."); + + $this->registry->add($foundLinks); + + Log::log("Links registered to database."); + + } + + private function findLinksInMeta(): void + { + Log::log("Finding links in post meta..."); + + $findLinkFromPostMeta = new \BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostMeta( + $this->wpService, + $this->config, + $this->db + ); + + $foundLinks = $findLinkFromPostMeta->findLinks(); + + Log::log("Found " . $foundLinks->getLinkCount() . " links in post meta."); + + $this->registry->add($foundLinks); + + Log::log("Links registered to database."); + } +} \ No newline at end of file diff --git a/source/php/Cli/Log.php b/source/php/Cli/Log.php new file mode 100644 index 0000000..82562dd --- /dev/null +++ b/source/php/Cli/Log.php @@ -0,0 +1,69 @@ +wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'broken_link_detector_db_version' + ); + } + + /** + * Get the current database version from the options table. + * + * @return string|null + */ + public function getDatabaseVersion(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + '2.0.0' + ); + } + + /** + * Get the name of the table that stores broken links. + * + * @return string + */ + public function getTableName(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'broken_links_detector' + ); + } + + /** + * Get plugin url. + * + * @return string + */ + public function getPluginUrl(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $this->pluginUrl + ); + } + + /** + * Get plugin url. + * + * @return string + */ + public function getPluginPath(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $this->pluginPath + ); + } + + /** + * Get location of fields + * + * @return string + */ + public function getPluginFieldsPath(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $this->getPluginPath() . 'source/fields' + ); + } + + /** + * Get text domain + * + * @return string + */ + public function getTextDomain(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'broken-link-detector' + ); + } + + /** + * Get post types where link repair (link updater) should not run. + * + * @return array + */ + public function linkUpdaterBannedPostTypes(): array + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + ['attachment', 'revision', 'acf', 'acf-field', 'acf-field-group'] + ) ?? []; + } + + /** + * Get post types that should not be checked for broken links. + * + * @return array + */ + public function linkDetectBannedPostTypes(): array { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + ['attachment', 'revision', 'acf', 'acf-field', 'acf-field-group'] + ) ?? []; + } + + /** + * Get post types that should not be checked for broken links. + * + * @return array + */ + public function linkDetectAllowedPostStatuses(): array { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + ['publish', 'private', 'password'] + ) ?? []; + } + + /** + * Get response codes that are considered broken. + * + * @return array + */ + public function responseCodesConsideredBroken(): array + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + [400, 403, 404, 410, 500, 502, 503, 504] + ) ?? []; + } + + /** + * Get the DNS record types to check. + * + * @return array + */ + public function checkIfDnsRespondsBeforeProbingUrl(): bool + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + true + ); + } + + /** + * Get the number of redirects to follow. + * + * @return int + */ + public function getMaxRedirects(): int + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 5 + ); + } + + /** + * Get the timeout for the request. + * + * @return int + */ + public function getTimeout(): int + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 5 + ); + } + + /** + * Get the interval for rechecking broken links. + * + * @return int The interval in minutes + */ + public function getRecheckInterval(): int + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 60 * 12 + ); + } + + /** + * Get the domains that should not be checked if broken or not. + * These will get registered, but will always be null. + * + * @return array + */ + public function getDomainsThatShouldNotBeChecked(): array + { + $domains = $this->acfService->getField( + 'broken_links_local_domains', + 'option' + ) ?: []; + + $domains = array_column($domains, 'domain'); + + $domains = array_map(function($domain) { + return parse_url($domain, PHP_URL_HOST); + }, $domains); + + $domains = array_filter($domains); + + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $domains ?? [] + ); + } + + /** + * Get the context check enabled status. + * + * @return bool + */ + public function isContextCheckEnabled() : bool { + $isEnabled = $this->acfService->getField( + 'broken_links_context_check_enabled', + 'option' + ) ?: false; + + $hasUrl = $this->getContextCheckUrl() ? true : false; + + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + ($isEnabled && $hasUrl) ?: false + ); + } + + /** + * Get the URL to probe for the context check. + * + * @return string + */ + public function getContextCheckUrl() : string { + $url = $this->acfService->getField( + 'broken_links_context_check_url', + 'option' + ) ?: false; + + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $url ?: '' + ); + } + + /** + * Get the timeout for the context check. + * + * @return int The timeout in milliseconds + */ + public function getContextCheckTimeout() : int { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 3000 + ); + } + + /** + * Get the domains that should be disabled when context failes. + * + * @return array + */ + public function getContextCheckDomainsToDisable(): array + { + $domains = $this->getDomainsThatShouldNotBeChecked(); + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $domains ?? [] + ); + } + + /** + * Get the class for the context check success. + * + * @return string + */ + public function getContextCheckSuccessClass() : string { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'context-check-avabile' + ); + } + + /** + * Get the class for the context check failed. + * + * @return string + */ + public function getContextCheckFailedClass() : string { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'context-check-unavabile' + ); + } + + /** + * Get the tooltip text for the context detection disabled link. + * + * @return string + */ + public function getContextCheckTooltipText(): string + { + $dbLabel = $this->acfService->getField( + 'broken_links_context_tooltip', + 'option' + ) ?: false; + + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + $dbLabel ?: $this->wpService->__( + 'Link unavabile', + 'broken-link-detector' + ) + ); + } + + /** + * Get the namespace for the WP CLI command. + * + * @return string + */ + public function getCommandNamespace(): string + { + return $this->wpService->applyFilters( + $this->createFilterKey(__FUNCTION__), + 'broken-link-detector' + ); + } + + /** + * Create a prefix for image conversion filter. + * + * @return string + */ + public function createFilterKey(string $filter = ""): string + { + return $this->filterPrefix . "/" . ucfirst($filter); + } +} \ No newline at end of file diff --git a/source/php/Config/ConfigInterface.php b/source/php/Config/ConfigInterface.php new file mode 100644 index 0000000..13bd693 --- /dev/null +++ b/source/php/Config/ConfigInterface.php @@ -0,0 +1,49 @@ + 1, + 'language' => 1, + 'field_loader' => 1, + + //Admin Features + 'admin_settings' => 1, + 'admin_summary' => 1, + 'admin_highlight_links' => 1, + + //On save autofixer + 'fix_internal_links' => 1, + 'maintain_link_registry' => 1, + + //CLI Features + 'cli' => 1, + 'cli_installer' => 1, + 'cli_link_finder' => 1, + 'cli_link_classifier' => 1, + + //Frontend context detection + 'context_detection' => 1, + 'frontend_styles' => 1, + ]; + + private function __construct(private string $feature) { + if (!isset(self::FEATURES[$feature])) { + throw new InvalidArgumentException("Feature {$feature} is not supported."); + } + } + + public function isEnabled(?int $version = null): bool + { + if (is_null($version)) { + return (bool) self::FEATURES[$this->feature]; + } + return (version_compare($version, self::FEATURES[$this->feature], 'eq')); + } + + public function getVersion(): int|false { + return self::FEATURES[$this->feature] ?: false; + } + + public static function factory(string $feature): Feature { + return new self($feature); + } +} \ No newline at end of file diff --git a/source/php/Config/FeatureInterface.php b/source/php/Config/FeatureInterface.php new file mode 100644 index 0000000..dff046b --- /dev/null +++ b/source/php/Config/FeatureInterface.php @@ -0,0 +1,10 @@ +wpService->getOption( + $this->config->getDatabaseVersionKey(), + null + ); + } + + /** + * The database table that broken link checker registeres broken stuff in. + * + * @return string + */ + public function getTableName(): string + { + return self::getInstance()->prefix . $this->config->getTableName(); + } + + /** + * Get the charset collation for the database. + * + * @return stringxx + */ + public function getCharsetCollation(): string + { + return self::getInstance()->get_charset_collate(); + } + + /** + * @return \wpdb + */ + public static function getInstance(): wpdb + { + static $db; + if ($db === null) { + global $wpdb; + $db = $wpdb; + } + return $db; + } +} \ No newline at end of file diff --git a/source/php/Database/DatabaseInterface.php b/source/php/Database/DatabaseInterface.php new file mode 100644 index 0000000..2cfac9a --- /dev/null +++ b/source/php/Database/DatabaseInterface.php @@ -0,0 +1,36 @@ +ID)) { - $plugins['brokenlinksdetector'] = BROKENLINKDETECTOR_URL . '/dist/js/mce-broken-link-detector.min.js'; - } - - return $plugins; - } - - public function getBrokenLinks() - { - global $post; - - if (!is_admin() ||!isset($post) ||empty($post->ID)) { - return; - } - - \BrokenLinkDetector\App::checkInstall(); - $urls = \BrokenLinkDetector\ListTable::getBrokenLinks($post->ID); - - echo ''; - } -} diff --git a/source/php/ExternalDetector.php b/source/php/ExternalDetector.php deleted file mode 100644 index 6c53ba7..0000000 --- a/source/php/ExternalDetector.php +++ /dev/null @@ -1,346 +0,0 @@ -lookForBrokenLinks($postId); - }); - - if (isset($_GET['broken-links-detector']) && $_GET['broken-links-detector'] == 'scan') { - do_action('broken-links-detector-external'); - } - } - - public function schedule() - { - if (wp_next_scheduled('broken-links-detector-external')) { - return; - } - - wp_schedule_event(time(), 'daily', 'broken-links-detector-external'); - } - - /** - * Look for broken links in post_content - * - * @param integer $post_id Optional post_id to update broken links for - * @return void - */ - public function lookForBrokenLinks($postId = null, $url = null) - { - \BrokenLinkDetector\App::checkInstall(); - $foundUrls = array(); - - if ($url) { - $url = "REGEXP ('.*(href=\"{$url}\").*')"; - } else { - $url = "RLIKE ('href=*')"; - } - - global $wpdb; - $sql = " - SELECT ID, post_content - FROM $wpdb->posts - WHERE - post_content {$url} - AND post_type NOT IN ('attachment', 'revision', 'acf', 'acf-field', 'acf-field-group') - AND post_status IN ('publish', 'private', 'password') - "; - - if (is_numeric($postId)) { - $sql .= " AND ID = $postId"; - } - - $posts = $wpdb->get_results($sql); - - if(is_array($posts) && !empty($posts)) { - foreach ($posts as $post) { - preg_match_all('/]+href=([\'"])(http|https)(.+?)\1[^>]*>/i', $post->post_content, $m); - - if (!isset($m[3]) || count($m[3]) > 0) { - foreach ($m[3] as $key => $url) { - $url = $m[2][$key] . $url; - - // Replace whitespaces in url - if (preg_match('/\s/', $url)) { - $newUrl = preg_replace('/ /', '%20', $url); - $wpdb->query( - $wpdb->prepare( - "UPDATE $wpdb->posts - SET post_content = REPLACE(post_content, %s, %s) - WHERE post_content LIKE %s - AND ID = %d", - $url, - $newUrl, - '%' . $wpdb->esc_like($url) . '%', - $post->ID - ) - ); - $url = $newUrl; - } - - if ($postId !== 'internal' && !$this->isBroken($url)) { - continue; - } - - $foundUrls[] = array( - 'post_id' => $post->ID, - 'url' => $url - ); - } - } - } - } - - /* START MODULE SUPPORT */ - if (is_numeric($postId)) { - $modules = array(); - $sql = "select meta_value from $wpdb->postmeta where meta_key='modularity-modules' and post_id = $postId"; - $modularity = $wpdb->get_results($sql); - foreach($modularity as $module) { - $data = unserialize($module->meta_value); - $keys = array_keys($data); - foreach($keys as $k) { - $modules[] = array_column($data[$k], 'postid'); - } - } - $modules = array_merge(...array_values($modules)); - foreach($modules as $mid) { - $meta = get_post_meta($mid, $key = '', $single = false ); - if(isset($meta['data']) && $meta['data'][0] > 0) { - for($i = 0; $i < $meta['data'][0]; $i++) { - $metakey = "data_".$i."_post_content"; - preg_match_all('/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i', $meta[$metakey][0], $matches); - foreach($matches[0] as $url) { - if (!$this->isBroken($url)) { - continue; - } - $foundUrls[] = array( - 'post_id' => $postId, - 'url' => $url - ); - } - } - } - $mpost = get_post($mid); - preg_match_all('/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i', $mpost->post_content, $matches); - foreach($matches[0] as $url) { - if (!$this->isBroken($url)) { - continue; - } - $foundUrls[] = array( - 'post_id' => $postId, - 'url' => $url - ); - } - } - } - /* END MODULE SUPPORT */ - - $this->saveBrokenLinks($foundUrls, $postId); - } - - /** - * Save broken links (if not already in db) - * @param array $data - * @return void - */ - public function saveBrokenLinks($data, $postId = null) - { - global $wpdb; - - $inserted = array(); - $tableName = \BrokenLinkDetector\App::$dbTable; - - if (is_numeric($postId)) { - $wpdb->delete($tableName, array('post_id' => $postId), array('%d')); - } elseif (is_null($postId)) { - $wpdb->query("TRUNCATE $tableName"); - } - - if(!empty($data) && is_array($data)) { - foreach ($data as $item) { - $exists = $wpdb->get_row("SELECT id FROM $tableName WHERE post_id = {$item['post_id']} AND url = '{$item['url']}'"); - - if ($exists) { - continue; - } - - $inserted[] = $wpdb->insert($tableName, array( - 'post_id' => $item['post_id'], - 'url' => $item['url'] - ), array('%d', '%s')); - } - } - - return true; - } - - /** - * Test if domain is valid with different methods - * @param string $url Url to check - * @return boolean - */ - public function isBroken($url) - { - if (!$domain = parse_url($url, PHP_URL_HOST)) { - return true; - } - - if(in_array($domain, (array) apply_filters('brokenLinks/External/ExceptedDomains', array()))) { - return false; - } - - // Convert domain name to IDNA ASCII form - if(count(explode('.', $domain)) == count(array_filter(explode('.', $domain), - function($var) { - if(strlen($var) < 1) { - return false; - } - return true; - }))) - { - try { - $domainAscii = idn_to_ascii($domain); - $url = str_ireplace($domain, $domainAscii, $url); - } catch (Exception $e) { - return false; - } - } - - // Test if URL is internal and page exist - if ($this->isInternal($url)) { - return false; - } - - // Validate domain name - if (!$this->isValidDomainName(isset($domainAscii) ? $domainAscii : $domain)) { - return true; - } - - // Test if domain is available - return !$this->isDomainAvailable($url); - } - - /** - * Test if domain name is valid - * @param string $domainName Url to check - * @return bool - */ - public function isValidDomainName($domainName) - { - return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domainName) - && preg_match("/^.{1,253}$/", $domainName) - && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domainName)); - } - - /** - * Test if domain is available with curl - * @param string $url Url to check - * @return bool - */ - public function isDomainAvailable($url, $timeOut = 7) - { - if (!function_exists('curl_init')) { - if(defined("BROKEN_LINKS_LOG") && BROKEN_LINKS_LOG) { - error_log("Broken links: Could not probe url " . $url . " due lack of curl in this environment."); - } - return true; - } - - // Init curl - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13'); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeOut); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_HTTPGET, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_MAXREDIRS, 5); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - - // Get the response - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curlError = curl_error($ch); - $curlErrorNo = curl_errno($ch); - curl_close($ch); - - //Curl response error - if($curlErrorNo) { - if(in_array($curlErrorNo, array(CURLE_TOO_MANY_REDIRECTS))) { - if(defined("BROKEN_LINKS_LOG") && BROKEN_LINKS_LOG) { - error_log("Broken links: Could not probe url " . $url . " due to a malfunction of curl [" . $curlErrorNo. " - " . $curlError . "]"); - } - return true; //Do not log - } else { - if(defined("BROKEN_LINKS_LOG") && BROKEN_LINKS_LOG) { - error_log("Broken links: Could not probe url " . $url . ", link is considerd broken [" . $curlErrorNo. " - " . $curlError . "]"); - } - return false; // Do log - } - } - - if(defined("BROKEN_LINKS_LOG") && BROKEN_LINKS_LOG) { - error_log("Broken links: Probe data " . $url . " [Curl error no: " . $curlErrorNo. "] [Curl error message:" . $curlError . "] [Http code: ".$httpCode."]"); - } - - //Validate - if($response) { - //Genereic codes - if($httpCode >= 200 && $httpCode < 400) { - return true; - } - - //Specific out of scope codes - //401: Unathorized - //406: Not acceptable - //413: Payload to large - //418: I'm a teapot - if(in_array($httpCode, array(401, 406, 413))) { - return true; - } - } - - return false; - } - - /** - * Test if URL is internal and page exist - * @param string $url Url to check - * @return bool - */ - public function isInternal($url) - { - // Check if post exist by url (only works with 'public' posts) - $postId = url_to_postid($url); - if ($postId > 0) { - return true; - } - - // Check if the URL is internal or external - $siteUrlComponents = parse_url(get_site_url()); - $urlComponents = parse_url($url); - if (!empty($siteUrlComponents['host']) && !empty($urlComponents['host']) && strcasecmp($urlComponents['host'], $siteUrlComponents['host']) === 0) { - // Test with get_page_by_path() to get other post statuses - $postTypes = get_post_types(array('public' => true)); - if (!empty($urlComponents['path']) && !empty(get_page_by_path(basename(untrailingslashit($urlComponents['path'])), ARRAY_A, $postTypes))) { - return true; - } - } - - return false; - } -} diff --git a/source/php/Fields/AcfExportManager/RegisterFieldConfiguration.php b/source/php/Fields/AcfExportManager/RegisterFieldConfiguration.php new file mode 100644 index 0000000..895ddc1 --- /dev/null +++ b/source/php/Fields/AcfExportManager/RegisterFieldConfiguration.php @@ -0,0 +1,35 @@ +fieldConfigurationDirectory)) { + throw new \InvalidArgumentException('Field configuration directory is required'); + } + } + + public function addHooks(): void + { + $this->wpService->addAction('acf/init', array($this, 'initFieldRegistration')); + } + + public function initFieldRegistration(): void + { + $acfExportManager = new \AcfExportManager\AcfExportManager(); + $acfExportManager->setTextdomain('api-event-manager'); + $acfExportManager->setExportFolder($this->fieldConfigurationDirectory ?? null); + $acfExportManager->autoExport(array( + 'local-domains' => 'group_6718e7ca78c94', + 'context-detection' => 'group_6718e9e8554ca', + )); + + $acfExportManager->import(); + } +} \ No newline at end of file diff --git a/source/php/Hooks/MaintainLinkRegistryOnSavePost.php b/source/php/Hooks/MaintainLinkRegistryOnSavePost.php new file mode 100644 index 0000000..2f3d0d3 --- /dev/null +++ b/source/php/Hooks/MaintainLinkRegistryOnSavePost.php @@ -0,0 +1,96 @@ +wpService->addAction('save_post', [$this, 'clearLinksOnSavePost'], 10, 3); + $this->wpService->addAction('save_post', [$this, 'findLinksOnSavePost'], 20, 3); + } + + /** + * The callback for the 'save_post' action. It will trigger the link clearing logic + * whenever a post is saved. + * + * @param int $postId The post ID. + * @param WP_Post $post The post object. + * @param bool $update Whether this is an update or a new post. + */ + public function clearLinksOnSavePost(int $postId, \WP_Post $post, bool $update): void + { + // Do not run on autosave or revisions + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + $this->registry->remove($postId); + } + + /** + * The callback for the 'save_post' action. It will trigger the link finding logic + * whenever a post is saved. + * + * @param int $postId The post ID. + * @param WP_Post $post The post object. + * @param bool $update Whether this is an update or a new post. + */ + public function findLinksOnSavePost(int $postId, \WP_Post $post, bool $update): void + { + // Do not run on autosave or revisions + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + // Collect links from post content and metadata + $this->findLinksInContent($postId); + $this->findLinksInMeta($postId); + } + + /** + * Find links in the content of the post and register them. + * + * @param int $postId The post ID. + */ + private function findLinksInContent(int $postId): void + { + $findLinkFromPostContent = new \BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostContent( + $this->wpService, + $this->config, + $this->db + ); + $foundLinks = $findLinkFromPostContent->findLinks($postId); + if($foundLinks->getLinkCount() !== 0) { + $this->registry->add($foundLinks); + } + } + + /** + * Find links in the post meta and register them. + * + * @param int $postId The post ID. + */ + private function findLinksInMeta(int $postId): void + { + $findLinkFromPostMeta = new \BrokenLinkDetector\BrokenLinkRegistry\FindLink\FindLinkFromPostMeta( + $this->wpService, + $this->config, + $this->db + ); + $foundLinks = $findLinkFromPostMeta->findLinks($postId); + if($foundLinks->getLinkCount() !== 0) { + $this->registry->add($foundLinks); + } + } +} \ No newline at end of file diff --git a/source/php/HooksRegistrar/Hookable.php b/source/php/HooksRegistrar/Hookable.php new file mode 100644 index 0000000..65223dd --- /dev/null +++ b/source/php/HooksRegistrar/Hookable.php @@ -0,0 +1,8 @@ +addHooks(); + + return $this; + } +} \ No newline at end of file diff --git a/source/php/HooksRegistrar/HooksRegistrar.test.php b/source/php/HooksRegistrar/HooksRegistrar.test.php new file mode 100644 index 0000000..0987c0c --- /dev/null +++ b/source/php/HooksRegistrar/HooksRegistrar.test.php @@ -0,0 +1,33 @@ +getHookableClass(); + $hooksRegistrar = new \BrokenLinkDetector\HooksRegistrar\HooksRegistrar(); + + ob_start(); + $hooksRegistrar->register($hookable); + + $this->assertEquals('Hooks added!', ob_get_clean()); + } + + private function getHookableClass(): Hookable + { + return new class implements Hookable { + public function addHooks(): void + { + echo 'Hooks added!'; + } + }; + } +} \ No newline at end of file diff --git a/source/php/HooksRegistrar/HooksRegistrarInterface.php b/source/php/HooksRegistrar/HooksRegistrarInterface.php new file mode 100644 index 0000000..5b74620 --- /dev/null +++ b/source/php/HooksRegistrar/HooksRegistrarInterface.php @@ -0,0 +1,10 @@ +getPluginPath())) { + throw new \InvalidArgumentException('The plugin path provided does not exist'); + } + } + + /** + * Registeres the activation and deactivation hooks + * + * @return void + */ + public function addHooks(): void + { + $this->wpService->registerActivationHook($this->config->getPluginPath(), array($this, 'install')); + $this->wpService->registerDeactivationHook($this->config->getPluginPath(), array($this, 'uninstall')); + + // Run install when the options page is saved acf. + $this->wpService->addAction('acf/save_post', array($this, 'install')); + } + + /** + * Runs the installation process + * + * @return void + */ + public function install(): void + { + $charsetCollation = $this->db->getCharsetCollation(); + $tableName = $this->db->getTableName(); + + //Delete the database, if the current + //version is not the same as the configured version + //The data in the database will be lost. + //This behavious is deemed acceptable, as + //the table will be repopulated at the next run. + if(!$this->isCurrentDatabaseVersion()) { + $this->uninstall(); + } + + if(!$this->isInstalled($tableName)) { + $installSql = "CREATE TABLE IF NOT EXISTS $tableName ( + id bigint(20) NOT NULL AUTO_INCREMENT, + post_id bigint(20) DEFAULT NULL, + url varchar(1024) NOT NULL DEFAULT '', + unique_hash char(32) NOT NULL, + http_code int(3) DEFAULT NULL, + time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY unique_post_url_hash (unique_hash) + ) $charsetCollation;"; + + //Require the upgrade.php file + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + + //Run the dbDelta function + dbDelta($installSql); + + //Save the current database version + $this->wpService->updateOption( + $this->config->getDatabaseVersionKey(), + $this->config->getDatabaseVersion() + ); + } + } + + /** + * Runs the uninstallation process + * + * @return void + */ + public function uninstall(): void + { + $this->db->getInstance()->query("DROP TABLE IF EXISTS " . $this->db->getTableName()); + $this->wpService->deleteOption($this->config->getDatabaseVersionKey()); + } + + /** + * Reinstalls the plugin, if needed + * + * @return void + */ + public function reinstall($force = true): void + { + if($force) { + $this->uninstall(); + } + $this->install(); + } + + /** + * Checks if the plugin database tables is installed + * + * @return bool + */ + private function isInstalled(string $tableName): bool + { + $isInstalledQuery = $this->db->getInstance()->prepare("SHOW TABLES LIKE %s", $tableName); + if($isInstalledQuery && $isInstalledQuery == $tableName) { + return true; + } + return false; + } + + /** + * Checks if the current database version is the same as the confgured database version + * + * @return bool + */ + private function isCurrentDatabaseVersion(): bool + { + return $this->db->getCurrentVersion() === $this->config->getDatabaseVersion(); + } + +} diff --git a/source/php/InstallerInterface.php b/source/php/InstallerInterface.php new file mode 100644 index 0000000..0bdc486 --- /dev/null +++ b/source/php/InstallerInterface.php @@ -0,0 +1,10 @@ +postId = (int) $postarr['ID']; - $this->getPermalinkBefore($this->postId); - add_action('save_post', array($this, 'getPermalinkAfter'), 10, 2); - } - } - - /** - * Get permalink before save - * @param integer $postId Post id - * @return void - */ - public function getPermalinkBefore($postId) - { - if (wp_is_post_revision($postId)) { - return; - } - - $this->permalinkBefore = get_permalink($postId); - } - - /** - * Get new permalink (after save) - * @param integer $postId Post id - * @return void - */ - public function getPermalinkAfter($postId, $post) - { - if (wp_is_post_revision($postId)) { - return; - } - - remove_action('save_post', array($this, 'getPermalinkAfter'), 10, 2); - - $this->permalinkAfter = get_permalink($this->postId); - - if ($post->post_status === 'trash') { - $this->trashed = true; - } - - if ($this->permalinkBefore && !empty($this->permalinkBefore)) { - $this->detectChangedPermalink(); - } - } - - /** - * Detect and repair - * @return void - */ - public function detectChangedPermalink() - { - // if permalink not changed, return, do nothing more - if ($this->permalinkBefore === $this->permalinkAfter && !$this->trashed) { - return false; - } - - if ($this->trashed) { - App::$externalDetector->lookForBrokenLinks('internal', str_replace('__trashed', '', $this->permalinkBefore)); - return true; - } - - // Replace occurances of the old permalink with the new permalink - global $wpdb; - - $wpdb->query( - $wpdb->prepare( - "UPDATE $wpdb->posts - SET post_content = REPLACE(post_content, %s, %s) - WHERE post_content LIKE %s", - $this->permalinkBefore, - $this->permalinkAfter, - '%' . $wpdb->esc_like($this->permalinkBefore) . '%' - ) - ); - - $this->permalinksUpdated += $wpdb->rows_affected; - - if ($this->permalinksUpdated > 0) { - add_notice(sprintf('%d links to this post was updated to use the new permalink.', $this->permalinksUpdated)); - } - - return true; - } -} diff --git a/source/php/LinkUpdater/LinkUpdater.php b/source/php/LinkUpdater/LinkUpdater.php new file mode 100644 index 0000000..68ae3e9 --- /dev/null +++ b/source/php/LinkUpdater/LinkUpdater.php @@ -0,0 +1,122 @@ +wpService->addFilter('wp_insert_post_data', array($this, 'updateLinks'), 10, 2); + } + + /** + * Update the links in the post content if the post name has changed + * @param array $data + * @param array $post + * @return bool + */ + public function updateLinks(array $data, array $post): array + { + if(is_null($post)) { + return $data; + } + + foreach(['post_type', 'post_name'] as $keys) { + if(!isset($data[$keys]) || !isset($post[$keys])) { + return $data; + } + } + + if($this->linkHasChanged($data, $post) && $this->shouldReplaceForPosttype($data['post_type'])) { + + $postId = $post['ID'] ?? null; + + if(is_numeric($postId)) { + $this->replaceLinks( + $this->createPermalink($postId, $data['post_name']), + $this->createPermalink($postId, $post['post_name']) + ); + $data['linkUpdated'] = true; + } + } + return $data; + } + + /** + * Replace the old link with the new link in posts that contains the link + * @param string $newLink + * @param string $oldLink + * @return int + */ + private function replaceLinks(string $newLink, string $oldLink): int + { + + $db = $this->database->getInstance(); + + $db->query( + $db->prepare( + "UPDATE $db->posts + SET post_content = REPLACE(post_content, %s, %s) + WHERE post_content LIKE %s", + $oldLink, + $newLink, + '%' . $db->esc_like($oldLink) . '%' + ) + ); + + return $db->rows_affected; + } + + /** + * Create a permalink from the post id and post name + * @param int $postId + * @param string $postName + * @return string + */ + public function createPermalink(int $postId, string $postName): string + { + $permalink = preg_replace('/[^\/]+\/?$/', + $postName, + $this->wpService->getPermalink($postId) + ); + + $permalink = rtrim($permalink, '/'); + + return $permalink; + } + + /** + * Check if the link has changed + * @param array $data The newly submitted data + * @param array $post The stored post data + * @return bool + */ + public function linkHasChanged(array $data, array $post): bool + { + return $data['post_name'] !== $post['post_name']; + } + + /** + * Check if the link should be replaced for the post type + * @param string $postType + * @return bool + */ + private function shouldReplaceForPosttype(string $postType): bool + { + return !in_array($postType, $this->config->linkUpdaterBannedPostTypes()); + } +} \ No newline at end of file diff --git a/source/php/LinkUpdater/LinkUpdater.test.php b/source/php/LinkUpdater/LinkUpdater.test.php new file mode 100644 index 0000000..13ba14c --- /dev/null +++ b/source/php/LinkUpdater/LinkUpdater.test.php @@ -0,0 +1,114 @@ + $input, + 'getOption' => 'option' + ]); + $acfService = new FakeAcfService([]); + + $config = new Config( + $wpService, + $acfService, + 'filter-prefix', + 'plugin-path', + 'plugin-url' + ); + + $postId = 123; + + $linkUpdater = new LinkUpdater( + $wpService, + $config, + new Database( + $config, + $wpService + ) + ); + + // Act + $result = $linkUpdater->createPermalink($postId, $postName); + + // Assert + $this->assertEquals($expected, $result); + } + + /** + * Test urls. + */ + public function testThatLinkHasChangedDetectsLinksThatChanges() + { + // Arrange + $wpService = new FakeWpService([ + 'getPermalink' => 'https://example.com/old-permalink/', + 'getOption' => 'option', + 'applyFilters' => function($filter, $value) { + return $value; + } + ]); + + $acfService = new FakeAcfService([]); + + $config = new Config( + $wpService, + $acfService, + 'filter-prefix', + 'plugin-path', + 'plugin-url' + ); + + $linkUpdater = new LinkUpdater( + $wpService, + $config, + new Database( + $config, + $wpService + ) + ); + + $data = [ + 'post_name' => 'sample-post', + 'post_type' => 'post' + ]; + + $post = [ + 'ID' => 123, + 'post_name' => 'old-post', + 'post_type' => 'post' + ]; + + $result = $linkUpdater->linkHasChanged($data, $post); + + // Assert + $this->assertTrue($result); + } + + /** + * Test urls. + */ + private function uriProvider() + { + return [ + ['https://example.com/old-permalink/', 'https://example.com/sample-post', 'sample-post'], + ['https://example.com/old-permalink', 'https://example.com/sample-post', 'sample-post'], + ['https://example.com/old-permalink/with/multiple/subs', 'https://example.com/old-permalink/with/multiple/sample-post', 'sample-post'] + ]; + } +} \ No newline at end of file diff --git a/source/php/LinkUpdater/LinkUpdaterInterface.php b/source/php/LinkUpdater/LinkUpdaterInterface.php new file mode 100644 index 0000000..62b8739 --- /dev/null +++ b/source/php/LinkUpdater/LinkUpdaterInterface.php @@ -0,0 +1,8 @@ +get_columns(); - $hidden = $this->get_hidden_columns(); - $sortable = $this->get_sortable_columns(); - - $data = self::getBrokenLinks(); - - $perPage = 30; - $currentPage = $this->get_pagenum(); - $totalItems = count($data); - $this->set_pagination_args(array( - 'total_items' => $totalItems, - 'per_page' => $perPage - )); - $data = array_slice($data, (($currentPage - 1) * $perPage), $perPage); - - $this->_column_headers = array($columns, $hidden, $sortable); - $this->items = $data; - } - - public static function getBrokenLinks($postId = false) - { - if (defined('DOING_AJAX') && DOING_AJAX) { - $postId = isset($_POST['post_id']) ? $_POST['post_id'] : false; - } - - \BrokenLinkDetector\App::checkInstall(); - - global $wpdb; - $tableName = \BrokenLinkDetector\App::$dbTable; - - $sql = "SELECT - links.*, - {$wpdb->posts}.*, - {$wpdb->posts}.ID AS post_id - FROM $tableName links - LEFT JOIN $wpdb->posts ON {$wpdb->posts}.ID = links.post_id"; - - if (is_numeric($postId)) { - $sql .= " WHERE {$wpdb->posts}.ID = $postId"; - } - - $sql .= " ORDER BY {$wpdb->posts}.post_title"; - - $result = $wpdb->get_results($sql); - - if (defined('DOING_AJAX') && DOING_AJAX) { - echo json_encode($result); - wp_die(); - } - - return $result; - } - - public static function getBrokenLinksCount($postId = false) - { - global $wpdb; - $tableName = \BrokenLinkDetector\App::$dbTable; - $sql = "SELECT - COUNT(*) AS length - FROM $tableName links - LEFT JOIN $wpdb->posts ON {$wpdb->posts}.ID = links.post_id"; - - if (is_numeric($postId)) { - $sql .= " WHERE {$wpdb->posts}.ID = $postId"; - } - - $sql .= " ORDER BY {$wpdb->posts}.post_title"; - - $result = $wpdb->get_var($sql); - - return $result; - } - - public function get_columns() - { - return array( - 'post' => __('Post', 'broken-link-detector'), - 'url' => __('Web adress', 'broken-link-detector'), - 'time' => __('Last probed', 'broken-link-detector') - ); - } - - public function get_hidden_columns() - { - return array(); - } - - public function get_sortable_columns() - { - return array( - 'post' => array('post', false), - 'url' => array('url', false), - 'time' => array('url', false), - ); - } - - public function column_default($item, $column_name) - { - switch ($column_name) { - case 'post': - return '' . $item->post_title . ''; - - case 'url': - return '' . $item->url . ''; - - default: - return $item->$column_name; - } - } -} diff --git a/source/php/TextDomain.php b/source/php/TextDomain.php new file mode 100644 index 0000000..a9e8fc2 --- /dev/null +++ b/source/php/TextDomain.php @@ -0,0 +1,29 @@ +wpService->addAction('plugins_loaded', array($this, 'loadTextDomain')); + } + + public function loadTextDomain(): void + { + $this->wpService->loadPluginTextDomain( + $this->config->gettextDomain(), + false, + $this->config->getPluginPath() . 'languages/' + ); + } +} \ No newline at end of file diff --git a/source/php/TextDomainInterface.php b/source/php/TextDomainInterface.php new file mode 100644 index 0000000..738e86c --- /dev/null +++ b/source/php/TextDomainInterface.php @@ -0,0 +1,8 @@ + - */ -class Psr4ClassLoader -{ - /** - * @var array - */ - private $prefixes = array(); - - /** - * @param string $prefix - * @param string $baseDir - */ - public function addPrefix($prefix, $baseDir) - { - $prefix = trim($prefix, '\\').'\\'; - $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; - $this->prefixes[] = array($prefix, $baseDir); - } - - /** - * @param string $class - * - * @return string|null - */ - public function findFile($class) - { - $class = ltrim($class, '\\'); - foreach ($this->prefixes as $current) { - list($currentPrefix, $currentBaseDir) = $current; - if (0 === strpos($class, $currentPrefix)) { - $classWithoutPrefix = substr($class, strlen($currentPrefix)); - $file = $currentBaseDir.str_replace('\\', DIRECTORY_SEPARATOR, $classWithoutPrefix).'.php'; - - if (file_exists($file)) { - return $file; - } - } - } - } - - /** - * @param string $class - * - * @return bool - */ - public function loadClass($class) - { - $file = $this->findFile($class); - if (null !== $file) { - require $file; - - return true; - } - - return false; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Removes this instance from the registered autoloaders. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } -} diff --git a/source/php/Vendor/admin-notice-helper.php b/source/php/Vendor/admin-notice-helper.php deleted file mode 100755 index 0f5740f..0000000 --- a/source/php/Vendor/admin-notice-helper.php +++ /dev/null @@ -1,121 +0,0 @@ - - * @return object - */ - public static function get_singleton() { - if ( ! isset( self::$instance ) ) { - self::$instance = new Admin_Notice_Helper(); - } - - return self::$instance; - } - - /** - * Initializes variables - */ - public function init() { - $default_notices = array( 'update' => array(), 'error' => array() ); - $this->notices = array_merge( $default_notices, get_option( 'anh_notices', array() ) ); - $this->notices_were_updated = false; - } - - /** - * Queues up a message to be displayed to the user - * - * @param string $message The text to show the user - * @param string $type 'update' for a success or notification message, or 'error' for an error message - */ - public function enqueue( $message, $type = 'update' ) { - if ( in_array( $message, array_values( $this->notices[ $type ] ) ) ) { - return; - } - - $this->notices[ $type ][] = (string) apply_filters( 'anh_enqueue_message', $message ); - $this->notices_were_updated = true; - } - - /** - * Displays updates and errors - */ - public function print_notices() { - foreach ( array( 'update', 'error' ) as $type ) { - if ( count( $this->notices[ $type ] ) ) { - $class = 'update' == $type ? 'updated' : 'error'; - - require( dirname( __FILE__ ) . '/admin-notice.php' ); - - $this->notices[ $type ] = array(); - $this->notices_were_updated = true; - } - } - } - - /** - * Writes notices to the database - */ - public function shutdown() { - if ( $this->notices_were_updated ) { - update_option( 'anh_notices', $this->notices ); - } - } - } // end Admin_Notice_Helper - - Admin_Notice_Helper::get_singleton(); // Create the instance immediately to make sure hook callbacks are registered in time - - if ( ! function_exists( 'add_notice' ) ) { - function add_notice( $message, $type = 'update' ) { - Admin_Notice_Helper::get_singleton()->enqueue( $message, $type ); - } - } -} diff --git a/source/php/Vendor/admin-notice.php b/source/php/Vendor/admin-notice.php deleted file mode 100755 index 1c6744a..0000000 --- a/source/php/Vendor/admin-notice.php +++ /dev/null @@ -1,5 +0,0 @@ -
- notices[ $type ] as $notice ) : ?> -

- -
diff --git a/source/sass/broken-link-detector.scss b/source/sass/broken-link-detector.scss index 26ed20e..5f262a6 100644 --- a/source/sass/broken-link-detector.scss +++ b/source/sass/broken-link-detector.scss @@ -1,14 +1,3 @@ -.broken-link-detector-label { - background-color: #ff0000; - border-radius: 5px; - box-sizing: border-box; - color: #fff; - display: inline-block; - font-size: 11px; - height: 2em; - line-height: 21px; - min-width: 24px; - padding: 0 8px; - text-align: center; - margin-top: 5px; +.broken-link-detector-link-is-unavabile { + cursor: not-allowed !important; } diff --git a/templates/list-table.php b/templates/list-table.php deleted file mode 100644 index df85117..0000000 --- a/templates/list-table.php +++ /dev/null @@ -1,11 +0,0 @@ -
-

-

Next broken links detection will run:

- -
- prepare_items(); - $listTable->display(); - ?> -
-
diff --git a/tests/bootstrap.default.php b/tests/bootstrap.default.php new file mode 100644 index 0000000..88ffd00 --- /dev/null +++ b/tests/bootstrap.default.php @@ -0,0 +1,6 @@ + [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-trunk + rm -rf $TMPDIR/wordpress-trunk/* + svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress + mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + rm -rf $WP_TESTS_DIR/{includes,data} + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +recreate_db() { + shopt -s nocasematch + if [[ $1 =~ ^(y|yes)$ ]] + then + mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + create_db + echo "Recreated the database ($DB_NAME)." + else + echo "Leaving the existing database ($DB_NAME) in place." + fi + shopt -u nocasematch +} + +create_db() { + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] + then + echo "Reinstalling will delete the existing test database ($DB_NAME)" + # read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB + DELETE_EXISTING_DB="y" + recreate_db $DELETE_EXISTING_DB + else + create_db + fi +} + +install_wp +install_test_suite +install_db diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1b03971 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "target": "es6", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "lib": [ + "ES2021", + "DOM", + "DOM.Iterable", + ], + "types": ["@types/tinymce"] + }, + "include": [ + "source/js/*.ts", + "source/js/*.js" + ] +} \ No newline at end of file diff --git a/uninstall.php b/uninstall.php deleted file mode 100644 index d388e54..0000000 --- a/uninstall.php +++ /dev/null @@ -1,11 +0,0 @@ -uninstall(); diff --git a/vendor/autoload.php b/vendor/autoload.php deleted file mode 100644 index 561517f..0000000 --- a/vendor/autoload.php +++ /dev/null @@ -1,7 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Autoload; - -/** - * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. - * - * $loader = new \Composer\Autoload\ClassLoader(); - * - * // register classes with namespaces - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * // activate the autoloader - * $loader->register(); - * - * // to enable searching the include path (eg. for PEAR packages) - * $loader->setUseIncludePath(true); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * This class is loosely based on the Symfony UniversalClassLoader. - * - * @author Fabien Potencier - * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ - */ -class ClassLoader -{ - // PSR-4 - private $prefixLengthsPsr4 = array(); - private $prefixDirsPsr4 = array(); - private $fallbackDirsPsr4 = array(); - - // PSR-0 - private $prefixesPsr0 = array(); - private $fallbackDirsPsr0 = array(); - - private $useIncludePath = false; - private $classMap = array(); - private $classMapAuthoritative = false; - private $missingClasses = array(); - private $apcuPrefix; - - public function getPrefixes() - { - if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); - } - - return array(); - } - - public function getPrefixesPsr4() - { - return $this->prefixDirsPsr4; - } - - public function getFallbackDirs() - { - return $this->fallbackDirsPsr0; - } - - public function getFallbackDirsPsr4() - { - return $this->fallbackDirsPsr4; - } - - public function getClassMap() - { - return $this->classMap; - } - - /** - * @param array $classMap Class to filename map - */ - public function addClassMap(array $classMap) - { - if ($this->classMap) { - $this->classMap = array_merge($this->classMap, $classMap); - } else { - $this->classMap = $classMap; - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, either - * appending or prepending to the ones previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories - */ - public function add($prefix, $paths, $prepend = false) - { - if (!$prefix) { - if ($prepend) { - $this->fallbackDirsPsr0 = array_merge( - (array) $paths, - $this->fallbackDirsPsr0 - ); - } else { - $this->fallbackDirsPsr0 = array_merge( - $this->fallbackDirsPsr0, - (array) $paths - ); - } - - return; - } - - $first = $prefix[0]; - if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; - - return; - } - if ($prepend) { - $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, - $this->prefixesPsr0[$first][$prefix] - ); - } else { - $this->prefixesPsr0[$first][$prefix] = array_merge( - $this->prefixesPsr0[$first][$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, either - * appending or prepending to the ones previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories - * - * @throws \InvalidArgumentException - */ - public function addPsr4($prefix, $paths, $prepend = false) - { - if (!$prefix) { - // Register directories for the root namespace. - if ($prepend) { - $this->fallbackDirsPsr4 = array_merge( - (array) $paths, - $this->fallbackDirsPsr4 - ); - } else { - $this->fallbackDirsPsr4 = array_merge( - $this->fallbackDirsPsr4, - (array) $paths - ); - } - } elseif (!isset($this->prefixDirsPsr4[$prefix])) { - // Register directories for a new namespace. - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } elseif ($prepend) { - // Prepend directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, - $this->prefixDirsPsr4[$prefix] - ); - } else { - // Append directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - $this->prefixDirsPsr4[$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, - * replacing any others previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 base directories - */ - public function set($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr0 = (array) $paths; - } else { - $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, - * replacing any others previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * - * @throws \InvalidArgumentException - */ - public function setPsr4($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr4 = (array) $paths; - } else { - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } - } - - /** - * Turns on searching the include path for class files. - * - * @param bool $useIncludePath - */ - public function setUseIncludePath($useIncludePath) - { - $this->useIncludePath = $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Turns off searching the prefix and fallback directories for classes - * that have not been registered with the class map. - * - * @param bool $classMapAuthoritative - */ - public function setClassMapAuthoritative($classMapAuthoritative) - { - $this->classMapAuthoritative = $classMapAuthoritative; - } - - /** - * Should class lookup fail if not found in the current class map? - * - * @return bool - */ - public function isClassMapAuthoritative() - { - return $this->classMapAuthoritative; - } - - /** - * APCu prefix to use to cache found/not-found classes, if the extension is enabled. - * - * @param string|null $apcuPrefix - */ - public function setApcuPrefix($apcuPrefix) - { - $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; - } - - /** - * The APCu prefix in use, or null if APCu caching is not enabled. - * - * @return string|null - */ - public function getApcuPrefix() - { - return $this->apcuPrefix; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * @return bool|null True if loaded, null otherwise - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - includeFile($file); - - return true; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|false The path if found, false otherwise - */ - public function findFile($class) - { - // class map lookup - if (isset($this->classMap[$class])) { - return $this->classMap[$class]; - } - if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { - return false; - } - if (null !== $this->apcuPrefix) { - $file = apcu_fetch($this->apcuPrefix.$class, $hit); - if ($hit) { - return $file; - } - } - - $file = $this->findFileWithExtension($class, '.php'); - - // Search for Hack files if we are running on HHVM - if (false === $file && defined('HHVM_VERSION')) { - $file = $this->findFileWithExtension($class, '.hh'); - } - - if (null !== $this->apcuPrefix) { - apcu_add($this->apcuPrefix.$class, $file); - } - - if (false === $file) { - // Remember that this class does not exist. - $this->missingClasses[$class] = true; - } - - return $file; - } - - private function findFileWithExtension($class, $ext) - { - // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; - - $first = $class[0]; - if (isset($this->prefixLengthsPsr4[$first])) { - $subPath = $class; - while (false !== $lastPos = strrpos($subPath, '\\')) { - $subPath = substr($subPath, 0, $lastPos); - $search = $subPath.'\\'; - if (isset($this->prefixDirsPsr4[$search])) { - $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); - foreach ($this->prefixDirsPsr4[$search] as $dir) { - if (file_exists($file = $dir . $pathEnd)) { - return $file; - } - } - } - } - } - - // PSR-4 fallback dirs - foreach ($this->fallbackDirsPsr4 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { - return $file; - } - } - - // PSR-0 lookup - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) - . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); - } else { - // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; - } - - if (isset($this->prefixesPsr0[$first])) { - foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - } - } - } - - // PSR-0 fallback dirs - foreach ($this->fallbackDirsPsr0 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - - // PSR-0 include paths. - if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { - return $file; - } - - return false; - } -} - -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - */ -function includeFile($file) -{ - include $file; -} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE deleted file mode 100644 index f27399a..0000000 --- a/vendor/composer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - -Copyright (c) Nils Adermann, Jordi Boggiano - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php deleted file mode 100644 index 7a91153..0000000 --- a/vendor/composer/autoload_classmap.php +++ /dev/null @@ -1,9 +0,0 @@ - $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', -); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php deleted file mode 100644 index b7fc012..0000000 --- a/vendor/composer/autoload_namespaces.php +++ /dev/null @@ -1,9 +0,0 @@ - array($vendorDir . '/true/punycode/src'), - 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), -); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php deleted file mode 100644 index 9923b9e..0000000 --- a/vendor/composer/autoload_real.php +++ /dev/null @@ -1,70 +0,0 @@ -= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); - if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInit101500a5841e1792888a8ec4dcead78a::getInitializer($loader)); - } else { - $map = require __DIR__ . '/autoload_namespaces.php'; - foreach ($map as $namespace => $path) { - $loader->set($namespace, $path); - } - - $map = require __DIR__ . '/autoload_psr4.php'; - foreach ($map as $namespace => $path) { - $loader->setPsr4($namespace, $path); - } - - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } - - $loader->register(true); - - if ($useStaticLoader) { - $includeFiles = Composer\Autoload\ComposerStaticInit101500a5841e1792888a8ec4dcead78a::$files; - } else { - $includeFiles = require __DIR__ . '/autoload_files.php'; - } - foreach ($includeFiles as $fileIdentifier => $file) { - composerRequire101500a5841e1792888a8ec4dcead78a($fileIdentifier, $file); - } - - return $loader; - } -} - -function composerRequire101500a5841e1792888a8ec4dcead78a($fileIdentifier, $file) -{ - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - require $file; - - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - } -} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php deleted file mode 100644 index 8524de4..0000000 --- a/vendor/composer/autoload_static.php +++ /dev/null @@ -1,43 +0,0 @@ - __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', - ); - - public static $prefixLengthsPsr4 = array ( - 'T' => - array ( - 'TrueBV\\' => 7, - ), - 'S' => - array ( - 'Symfony\\Polyfill\\Mbstring\\' => 26, - ), - ); - - public static $prefixDirsPsr4 = array ( - 'TrueBV\\' => - array ( - 0 => __DIR__ . '/..' . '/true/punycode/src', - ), - 'Symfony\\Polyfill\\Mbstring\\' => - array ( - 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', - ), - ); - - public static function getInitializer(ClassLoader $loader) - { - return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInit101500a5841e1792888a8ec4dcead78a::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInit101500a5841e1792888a8ec4dcead78a::$prefixDirsPsr4; - - }, null, ClassLoader::class); - } -} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json deleted file mode 100644 index d3a6525..0000000 --- a/vendor/composer/installed.json +++ /dev/null @@ -1,111 +0,0 @@ -[ - { - "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", - "version_normalized": "1.9.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "time": "2018-08-06T14:22:27+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9-dev" - } - }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ] - }, - { - "name": "true/punycode", - "version": "v2.1.1", - "version_normalized": "2.1.1.0", - "source": { - "type": "git", - "url": "https://github.com/true/php-punycode.git", - "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", - "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "symfony/polyfill-mbstring": "^1.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.7", - "squizlabs/php_codesniffer": "~2.0" - }, - "time": "2016-11-16T10:37:54+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "TrueBV\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Renan Gonçalves", - "email": "renan.saddam@gmail.com" - } - ], - "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", - "homepage": "https://github.com/true/php-punycode", - "keywords": [ - "idna", - "punycode" - ] - } -] diff --git a/vendor/symfony/polyfill-mbstring/LICENSE b/vendor/symfony/polyfill-mbstring/LICENSE deleted file mode 100644 index 24fa32c..0000000 --- a/vendor/symfony/polyfill-mbstring/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2015-2018 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/symfony/polyfill-mbstring/Mbstring.php b/vendor/symfony/polyfill-mbstring/Mbstring.php deleted file mode 100644 index 1f568b4..0000000 --- a/vendor/symfony/polyfill-mbstring/Mbstring.php +++ /dev/null @@ -1,789 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Polyfill\Mbstring; - -/** - * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. - * - * Implemented: - * - mb_chr - Returns a specific character from its Unicode code point - * - mb_convert_encoding - Convert character encoding - * - mb_convert_variables - Convert character code in variable(s) - * - mb_decode_mimeheader - Decode string in MIME header field - * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED - * - mb_decode_numericentity - Decode HTML numeric string reference to character - * - mb_encode_numericentity - Encode character to HTML numeric string reference - * - mb_convert_case - Perform case folding on a string - * - mb_detect_encoding - Detect character encoding - * - mb_get_info - Get internal settings of mbstring - * - mb_http_input - Detect HTTP input character encoding - * - mb_http_output - Set/Get HTTP output character encoding - * - mb_internal_encoding - Set/Get internal character encoding - * - mb_list_encodings - Returns an array of all supported encodings - * - mb_ord - Returns the Unicode code point of a character - * - mb_output_handler - Callback function converts character encoding in output buffer - * - mb_scrub - Replaces ill-formed byte sequences with substitute characters - * - mb_strlen - Get string length - * - mb_strpos - Find position of first occurrence of string in a string - * - mb_strrpos - Find position of last occurrence of a string in a string - * - mb_strtolower - Make a string lowercase - * - mb_strtoupper - Make a string uppercase - * - mb_substitute_character - Set/Get substitution character - * - mb_substr - Get part of string - * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive - * - mb_stristr - Finds first occurrence of a string within another, case insensitive - * - mb_strrchr - Finds the last occurrence of a character in a string within another - * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive - * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive - * - mb_strstr - Finds first occurrence of a string within another - * - mb_strwidth - Return width of string - * - mb_substr_count - Count the number of substring occurrences - * - * Not implemented: - * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) - * - mb_ereg_* - Regular expression with multibyte support - * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable - * - mb_preferred_mime_name - Get MIME charset string - * - mb_regex_encoding - Returns current encoding for multibyte regex as string - * - mb_regex_set_options - Set/Get the default options for mbregex functions - * - mb_send_mail - Send encoded mail - * - mb_split - Split multibyte string using regular expression - * - mb_strcut - Get part of string - * - mb_strimwidth - Get truncated string with specified width - * - * @author Nicolas Grekas - * - * @internal - */ -final class Mbstring -{ - const MB_CASE_FOLD = PHP_INT_MAX; - - private static $encodingList = array('ASCII', 'UTF-8'); - private static $language = 'neutral'; - private static $internalEncoding = 'UTF-8'; - private static $caseFold = array( - array('µ','ſ',"\xCD\x85",'ς',"\xCF\x90","\xCF\x91","\xCF\x95","\xCF\x96","\xCF\xB0","\xCF\xB1","\xCF\xB5","\xE1\xBA\x9B","\xE1\xBE\xBE"), - array('μ','s','ι', 'σ','β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1",'ι'), - ); - - public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) - { - if (\is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) { - $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); - } else { - $fromEncoding = self::getEncoding($fromEncoding); - } - - $toEncoding = self::getEncoding($toEncoding); - - if ('BASE64' === $fromEncoding) { - $s = base64_decode($s); - $fromEncoding = $toEncoding; - } - - if ('BASE64' === $toEncoding) { - return base64_encode($s); - } - - if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { - if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { - $fromEncoding = 'Windows-1252'; - } - if ('UTF-8' !== $fromEncoding) { - $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); - } - - return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s); - } - - if ('HTML-ENTITIES' === $fromEncoding) { - $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8'); - $fromEncoding = 'UTF-8'; - } - - return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); - } - - public static function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) - { - $vars = array(&$a, &$b, &$c, &$d, &$e, &$f); - - $ok = true; - array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { - if (false === $v = Mbstring::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { - $ok = false; - } - }); - - return $ok ? $fromEncoding : false; - } - - public static function mb_decode_mimeheader($s) - { - return iconv_mime_decode($s, 2, self::$internalEncoding); - } - - public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) - { - trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', E_USER_WARNING); - } - - public static function mb_decode_numericentity($s, $convmap, $encoding = null) - { - if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { - trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.gettype($s).' given', E_USER_WARNING); - return null; - } - - if (!\is_array($convmap) || !$convmap) { - return false; - } - - if (null !== $encoding && !\is_scalar($encoding)) { - trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.gettype($s).' given', E_USER_WARNING); - return ''; // Instead of null (cf. mb_encode_numericentity). - } - - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - $cnt = floor(\count($convmap) / 4) * 4; - - for ($i = 0; $i < $cnt; $i += 4) { - // collector_decode_htmlnumericentity ignores $convmap[$i + 3] - $convmap[$i] += $convmap[$i + 2]; - $convmap[$i + 1] += $convmap[$i + 2]; - } - - $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { - $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; - for ($i = 0; $i < $cnt; $i += 4) { - if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { - return Mbstring::mb_chr($c - $convmap[$i + 2]); - } - } - return $m[0]; - }, $s); - - if (null === $encoding) { - return $s; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $s); - } - - public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) - { - if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { - trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.gettype($s).' given', E_USER_WARNING); - return null; - } - - if (!\is_array($convmap) || !$convmap) { - return false; - } - - if (null !== $encoding && !\is_scalar($encoding)) { - trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.gettype($s).' given', E_USER_WARNING); - return null; // Instead of '' (cf. mb_decode_numericentity). - } - - if (null !== $is_hex && !\is_scalar($is_hex)) { - trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.gettype($s).' given', E_USER_WARNING); - return null; - } - - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); - - $cnt = floor(\count($convmap) / 4) * 4; - $i = 0; - $len = \strlen($s); - $result = ''; - - while ($i < $len) { - $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; - $uchr = substr($s, $i, $ulen); - $i += $ulen; - $c = self::mb_ord($uchr); - - for ($j = 0; $j < $cnt; $j += 4) { - if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { - $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; - $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; - continue 2; - } - } - $result .= $uchr; - } - - if (null === $encoding) { - return $result; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $result); - } - - public static function mb_convert_case($s, $mode, $encoding = null) - { - $s = (string) $s; - if ('' === $s) { - return ''; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding) { - $encoding = null; - if (!preg_match('//u', $s)) { - $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); - } - } else { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - if (MB_CASE_TITLE == $mode) { - static $titleRegexp = null; - if (null === $titleRegexp) { - $titleRegexp = self::getData('titleCaseRegexp'); - } - $s = preg_replace_callback($titleRegexp, array(__CLASS__, 'title_case'), $s); - } else { - if (MB_CASE_UPPER == $mode) { - static $upper = null; - if (null === $upper) { - $upper = self::getData('upperCase'); - } - $map = $upper; - } else { - if (self::MB_CASE_FOLD === $mode) { - $s = str_replace(self::$caseFold[0], self::$caseFold[1], $s); - } - - static $lower = null; - if (null === $lower) { - $lower = self::getData('lowerCase'); - } - $map = $lower; - } - - static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); - - $i = 0; - $len = \strlen($s); - - while ($i < $len) { - $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; - $uchr = substr($s, $i, $ulen); - $i += $ulen; - - if (isset($map[$uchr])) { - $uchr = $map[$uchr]; - $nlen = \strlen($uchr); - - if ($nlen == $ulen) { - $nlen = $i; - do { - $s[--$nlen] = $uchr[--$ulen]; - } while ($ulen); - } else { - $s = substr_replace($s, $uchr, $i - $ulen, $ulen); - $len += $nlen - $ulen; - $i += $nlen - $ulen; - } - } - } - } - - if (null === $encoding) { - return $s; - } - - return iconv('UTF-8', $encoding.'//IGNORE', $s); - } - - public static function mb_internal_encoding($encoding = null) - { - if (null === $encoding) { - return self::$internalEncoding; - } - - $encoding = self::getEncoding($encoding); - - if ('UTF-8' === $encoding || false !== @iconv($encoding, $encoding, ' ')) { - self::$internalEncoding = $encoding; - - return true; - } - - return false; - } - - public static function mb_language($lang = null) - { - if (null === $lang) { - return self::$language; - } - - switch ($lang = strtolower($lang)) { - case 'uni': - case 'neutral': - self::$language = $lang; - - return true; - } - - return false; - } - - public static function mb_list_encodings() - { - return array('UTF-8'); - } - - public static function mb_encoding_aliases($encoding) - { - switch (strtoupper($encoding)) { - case 'UTF8': - case 'UTF-8': - return array('utf8'); - } - - return false; - } - - public static function mb_check_encoding($var = null, $encoding = null) - { - if (null === $encoding) { - if (null === $var) { - return false; - } - $encoding = self::$internalEncoding; - } - - return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var); - } - - public static function mb_detect_encoding($str, $encodingList = null, $strict = false) - { - if (null === $encodingList) { - $encodingList = self::$encodingList; - } else { - if (!\is_array($encodingList)) { - $encodingList = array_map('trim', explode(',', $encodingList)); - } - $encodingList = array_map('strtoupper', $encodingList); - } - - foreach ($encodingList as $enc) { - switch ($enc) { - case 'ASCII': - if (!preg_match('/[\x80-\xFF]/', $str)) { - return $enc; - } - break; - - case 'UTF8': - case 'UTF-8': - if (preg_match('//u', $str)) { - return 'UTF-8'; - } - break; - - default: - if (0 === strncmp($enc, 'ISO-8859-', 9)) { - return $enc; - } - } - } - - return false; - } - - public static function mb_detect_order($encodingList = null) - { - if (null === $encodingList) { - return self::$encodingList; - } - - if (!\is_array($encodingList)) { - $encodingList = array_map('trim', explode(',', $encodingList)); - } - $encodingList = array_map('strtoupper', $encodingList); - - foreach ($encodingList as $enc) { - switch ($enc) { - default: - if (strncmp($enc, 'ISO-8859-', 9)) { - return false; - } - case 'ASCII': - case 'UTF8': - case 'UTF-8': - } - } - - self::$encodingList = $encodingList; - - return true; - } - - public static function mb_strlen($s, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return \strlen($s); - } - - return @iconv_strlen($s, $encoding); - } - - public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return strpos($haystack, $needle, $offset); - } - - $needle = (string) $needle; - if ('' === $needle) { - trigger_error(__METHOD__.': Empty delimiter', E_USER_WARNING); - - return false; - } - - return iconv_strpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return strrpos($haystack, $needle, $offset); - } - - if ($offset != (int) $offset) { - $offset = 0; - } elseif ($offset = (int) $offset) { - if ($offset < 0) { - $haystack = self::mb_substr($haystack, 0, $offset, $encoding); - $offset = 0; - } else { - $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); - } - } - - $pos = iconv_strrpos($haystack, $needle, $encoding); - - return false !== $pos ? $offset + $pos : false; - } - - public static function mb_strtolower($s, $encoding = null) - { - return self::mb_convert_case($s, MB_CASE_LOWER, $encoding); - } - - public static function mb_strtoupper($s, $encoding = null) - { - return self::mb_convert_case($s, MB_CASE_UPPER, $encoding); - } - - public static function mb_substitute_character($c = null) - { - if (0 === strcasecmp($c, 'none')) { - return true; - } - - return null !== $c ? false : 'none'; - } - - public static function mb_substr($s, $start, $length = null, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return substr($s, $start, null === $length ? 2147483647 : $length); - } - - if ($start < 0) { - $start = iconv_strlen($s, $encoding) + $start; - if ($start < 0) { - $start = 0; - } - } - - if (null === $length) { - $length = 2147483647; - } elseif ($length < 0) { - $length = iconv_strlen($s, $encoding) + $length - $start; - if ($length < 0) { - return ''; - } - } - - return (string) iconv_substr($s, $start, $length, $encoding); - } - - public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) - { - $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); - $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); - - return self::mb_strpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) - { - $pos = self::mb_stripos($haystack, $needle, 0, $encoding); - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) - { - $encoding = self::getEncoding($encoding); - if ('CP850' === $encoding || 'ASCII' === $encoding) { - return strrchr($haystack, $needle, $part); - } - $needle = self::mb_substr($needle, 0, 1, $encoding); - $pos = iconv_strrpos($haystack, $needle, $encoding); - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) - { - $needle = self::mb_substr($needle, 0, 1, $encoding); - $pos = self::mb_strripos($haystack, $needle, $encoding); - - return self::getSubpart($pos, $part, $haystack, $encoding); - } - - public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) - { - $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); - $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); - - return self::mb_strrpos($haystack, $needle, $offset, $encoding); - } - - public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) - { - $pos = strpos($haystack, $needle); - if (false === $pos) { - return false; - } - if ($part) { - return substr($haystack, 0, $pos); - } - - return substr($haystack, $pos); - } - - public static function mb_get_info($type = 'all') - { - $info = array( - 'internal_encoding' => self::$internalEncoding, - 'http_output' => 'pass', - 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', - 'func_overload' => 0, - 'func_overload_list' => 'no overload', - 'mail_charset' => 'UTF-8', - 'mail_header_encoding' => 'BASE64', - 'mail_body_encoding' => 'BASE64', - 'illegal_chars' => 0, - 'encoding_translation' => 'Off', - 'language' => self::$language, - 'detect_order' => self::$encodingList, - 'substitute_character' => 'none', - 'strict_detection' => 'Off', - ); - - if ('all' === $type) { - return $info; - } - if (isset($info[$type])) { - return $info[$type]; - } - - return false; - } - - public static function mb_http_input($type = '') - { - return false; - } - - public static function mb_http_output($encoding = null) - { - return null !== $encoding ? 'pass' === $encoding : 'pass'; - } - - public static function mb_strwidth($s, $encoding = null) - { - $encoding = self::getEncoding($encoding); - - if ('UTF-8' !== $encoding) { - $s = iconv($encoding, 'UTF-8//IGNORE', $s); - } - - $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); - - return ($wide << 1) + iconv_strlen($s, 'UTF-8'); - } - - public static function mb_substr_count($haystack, $needle, $encoding = null) - { - return substr_count($haystack, $needle); - } - - public static function mb_output_handler($contents, $status) - { - return $contents; - } - - public static function mb_chr($code, $encoding = null) - { - if (0x80 > $code %= 0x200000) { - $s = \chr($code); - } elseif (0x800 > $code) { - $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); - } elseif (0x10000 > $code) { - $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } else { - $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); - } - - if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { - $s = mb_convert_encoding($s, $encoding, 'UTF-8'); - } - - return $s; - } - - public static function mb_ord($s, $encoding = null) - { - if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { - $s = mb_convert_encoding($s, 'UTF-8', $encoding); - } - - $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; - if (0xF0 <= $code) { - return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; - } - if (0xE0 <= $code) { - return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; - } - if (0xC0 <= $code) { - return (($code - 0xC0) << 6) + $s[2] - 0x80; - } - - return $code; - } - - private static function getSubpart($pos, $part, $haystack, $encoding) - { - if (false === $pos) { - return false; - } - if ($part) { - return self::mb_substr($haystack, 0, $pos, $encoding); - } - - return self::mb_substr($haystack, $pos, null, $encoding); - } - - private static function html_encoding_callback(array $m) - { - $i = 1; - $entities = ''; - $m = unpack('C*', htmlentities($m[0], ENT_COMPAT, 'UTF-8')); - - while (isset($m[$i])) { - if (0x80 > $m[$i]) { - $entities .= \chr($m[$i++]); - continue; - } - if (0xF0 <= $m[$i]) { - $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; - } elseif (0xE0 <= $m[$i]) { - $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; - } else { - $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; - } - - $entities .= '&#'.$c.';'; - } - - return $entities; - } - - private static function title_case(array $s) - { - return self::mb_convert_case($s[1], MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], MB_CASE_LOWER, 'UTF-8'); - } - - private static function getData($file) - { - if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { - return require $file; - } - - return false; - } - - private static function getEncoding($encoding) - { - if (null === $encoding) { - return self::$internalEncoding; - } - - $encoding = strtoupper($encoding); - - if ('8BIT' === $encoding || 'BINARY' === $encoding) { - return 'CP850'; - } - if ('UTF8' === $encoding) { - return 'UTF-8'; - } - - return $encoding; - } -} diff --git a/vendor/symfony/polyfill-mbstring/README.md b/vendor/symfony/polyfill-mbstring/README.md deleted file mode 100644 index 342e828..0000000 --- a/vendor/symfony/polyfill-mbstring/README.md +++ /dev/null @@ -1,13 +0,0 @@ -Symfony Polyfill / Mbstring -=========================== - -This component provides a partial, native PHP implementation for the -[Mbstring](http://php.net/mbstring) extension. - -More information can be found in the -[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). - -License -======= - -This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php deleted file mode 100644 index 3ca1641..0000000 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php +++ /dev/null @@ -1,1101 +0,0 @@ - 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', - 'E' => 'e', - 'F' => 'f', - 'G' => 'g', - 'H' => 'h', - 'I' => 'i', - 'J' => 'j', - 'K' => 'k', - 'L' => 'l', - 'M' => 'm', - 'N' => 'n', - 'O' => 'o', - 'P' => 'p', - 'Q' => 'q', - 'R' => 'r', - 'S' => 's', - 'T' => 't', - 'U' => 'u', - 'V' => 'v', - 'W' => 'w', - 'X' => 'x', - 'Y' => 'y', - 'Z' => 'z', - 'À' => 'à', - 'Á' => 'á', - 'Â' => 'â', - 'Ã' => 'ã', - 'Ä' => 'ä', - 'Å' => 'å', - 'Æ' => 'æ', - 'Ç' => 'ç', - 'È' => 'è', - 'É' => 'é', - 'Ê' => 'ê', - 'Ë' => 'ë', - 'Ì' => 'ì', - 'Í' => 'í', - 'Î' => 'î', - 'Ï' => 'ï', - 'Ð' => 'ð', - 'Ñ' => 'ñ', - 'Ò' => 'ò', - 'Ó' => 'ó', - 'Ô' => 'ô', - 'Õ' => 'õ', - 'Ö' => 'ö', - 'Ø' => 'ø', - 'Ù' => 'ù', - 'Ú' => 'ú', - 'Û' => 'û', - 'Ü' => 'ü', - 'Ý' => 'ý', - 'Þ' => 'þ', - 'Ā' => 'ā', - 'Ă' => 'ă', - 'Ą' => 'ą', - 'Ć' => 'ć', - 'Ĉ' => 'ĉ', - 'Ċ' => 'ċ', - 'Č' => 'č', - 'Ď' => 'ď', - 'Đ' => 'đ', - 'Ē' => 'ē', - 'Ĕ' => 'ĕ', - 'Ė' => 'ė', - 'Ę' => 'ę', - 'Ě' => 'ě', - 'Ĝ' => 'ĝ', - 'Ğ' => 'ğ', - 'Ġ' => 'ġ', - 'Ģ' => 'ģ', - 'Ĥ' => 'ĥ', - 'Ħ' => 'ħ', - 'Ĩ' => 'ĩ', - 'Ī' => 'ī', - 'Ĭ' => 'ĭ', - 'Į' => 'į', - 'İ' => 'i', - 'IJ' => 'ij', - 'Ĵ' => 'ĵ', - 'Ķ' => 'ķ', - 'Ĺ' => 'ĺ', - 'Ļ' => 'ļ', - 'Ľ' => 'ľ', - 'Ŀ' => 'ŀ', - 'Ł' => 'ł', - 'Ń' => 'ń', - 'Ņ' => 'ņ', - 'Ň' => 'ň', - 'Ŋ' => 'ŋ', - 'Ō' => 'ō', - 'Ŏ' => 'ŏ', - 'Ő' => 'ő', - 'Œ' => 'œ', - 'Ŕ' => 'ŕ', - 'Ŗ' => 'ŗ', - 'Ř' => 'ř', - 'Ś' => 'ś', - 'Ŝ' => 'ŝ', - 'Ş' => 'ş', - 'Š' => 'š', - 'Ţ' => 'ţ', - 'Ť' => 'ť', - 'Ŧ' => 'ŧ', - 'Ũ' => 'ũ', - 'Ū' => 'ū', - 'Ŭ' => 'ŭ', - 'Ů' => 'ů', - 'Ű' => 'ű', - 'Ų' => 'ų', - 'Ŵ' => 'ŵ', - 'Ŷ' => 'ŷ', - 'Ÿ' => 'ÿ', - 'Ź' => 'ź', - 'Ż' => 'ż', - 'Ž' => 'ž', - 'Ɓ' => 'ɓ', - 'Ƃ' => 'ƃ', - 'Ƅ' => 'ƅ', - 'Ɔ' => 'ɔ', - 'Ƈ' => 'ƈ', - 'Ɖ' => 'ɖ', - 'Ɗ' => 'ɗ', - 'Ƌ' => 'ƌ', - 'Ǝ' => 'ǝ', - 'Ə' => 'ə', - 'Ɛ' => 'ɛ', - 'Ƒ' => 'ƒ', - 'Ɠ' => 'ɠ', - 'Ɣ' => 'ɣ', - 'Ɩ' => 'ɩ', - 'Ɨ' => 'ɨ', - 'Ƙ' => 'ƙ', - 'Ɯ' => 'ɯ', - 'Ɲ' => 'ɲ', - 'Ɵ' => 'ɵ', - 'Ơ' => 'ơ', - 'Ƣ' => 'ƣ', - 'Ƥ' => 'ƥ', - 'Ʀ' => 'ʀ', - 'Ƨ' => 'ƨ', - 'Ʃ' => 'ʃ', - 'Ƭ' => 'ƭ', - 'Ʈ' => 'ʈ', - 'Ư' => 'ư', - 'Ʊ' => 'ʊ', - 'Ʋ' => 'ʋ', - 'Ƴ' => 'ƴ', - 'Ƶ' => 'ƶ', - 'Ʒ' => 'ʒ', - 'Ƹ' => 'ƹ', - 'Ƽ' => 'ƽ', - 'DŽ' => 'dž', - 'Dž' => 'dž', - 'LJ' => 'lj', - 'Lj' => 'lj', - 'NJ' => 'nj', - 'Nj' => 'nj', - 'Ǎ' => 'ǎ', - 'Ǐ' => 'ǐ', - 'Ǒ' => 'ǒ', - 'Ǔ' => 'ǔ', - 'Ǖ' => 'ǖ', - 'Ǘ' => 'ǘ', - 'Ǚ' => 'ǚ', - 'Ǜ' => 'ǜ', - 'Ǟ' => 'ǟ', - 'Ǡ' => 'ǡ', - 'Ǣ' => 'ǣ', - 'Ǥ' => 'ǥ', - 'Ǧ' => 'ǧ', - 'Ǩ' => 'ǩ', - 'Ǫ' => 'ǫ', - 'Ǭ' => 'ǭ', - 'Ǯ' => 'ǯ', - 'DZ' => 'dz', - 'Dz' => 'dz', - 'Ǵ' => 'ǵ', - 'Ƕ' => 'ƕ', - 'Ƿ' => 'ƿ', - 'Ǹ' => 'ǹ', - 'Ǻ' => 'ǻ', - 'Ǽ' => 'ǽ', - 'Ǿ' => 'ǿ', - 'Ȁ' => 'ȁ', - 'Ȃ' => 'ȃ', - 'Ȅ' => 'ȅ', - 'Ȇ' => 'ȇ', - 'Ȉ' => 'ȉ', - 'Ȋ' => 'ȋ', - 'Ȍ' => 'ȍ', - 'Ȏ' => 'ȏ', - 'Ȑ' => 'ȑ', - 'Ȓ' => 'ȓ', - 'Ȕ' => 'ȕ', - 'Ȗ' => 'ȗ', - 'Ș' => 'ș', - 'Ț' => 'ț', - 'Ȝ' => 'ȝ', - 'Ȟ' => 'ȟ', - 'Ƞ' => 'ƞ', - 'Ȣ' => 'ȣ', - 'Ȥ' => 'ȥ', - 'Ȧ' => 'ȧ', - 'Ȩ' => 'ȩ', - 'Ȫ' => 'ȫ', - 'Ȭ' => 'ȭ', - 'Ȯ' => 'ȯ', - 'Ȱ' => 'ȱ', - 'Ȳ' => 'ȳ', - 'Ⱥ' => 'ⱥ', - 'Ȼ' => 'ȼ', - 'Ƚ' => 'ƚ', - 'Ⱦ' => 'ⱦ', - 'Ɂ' => 'ɂ', - 'Ƀ' => 'ƀ', - 'Ʉ' => 'ʉ', - 'Ʌ' => 'ʌ', - 'Ɇ' => 'ɇ', - 'Ɉ' => 'ɉ', - 'Ɋ' => 'ɋ', - 'Ɍ' => 'ɍ', - 'Ɏ' => 'ɏ', - 'Ͱ' => 'ͱ', - 'Ͳ' => 'ͳ', - 'Ͷ' => 'ͷ', - 'Ϳ' => 'ϳ', - 'Ά' => 'ά', - 'Έ' => 'έ', - 'Ή' => 'ή', - 'Ί' => 'ί', - 'Ό' => 'ό', - 'Ύ' => 'ύ', - 'Ώ' => 'ώ', - 'Α' => 'α', - 'Β' => 'β', - 'Γ' => 'γ', - 'Δ' => 'δ', - 'Ε' => 'ε', - 'Ζ' => 'ζ', - 'Η' => 'η', - 'Θ' => 'θ', - 'Ι' => 'ι', - 'Κ' => 'κ', - 'Λ' => 'λ', - 'Μ' => 'μ', - 'Ν' => 'ν', - 'Ξ' => 'ξ', - 'Ο' => 'ο', - 'Π' => 'π', - 'Ρ' => 'ρ', - 'Σ' => 'σ', - 'Τ' => 'τ', - 'Υ' => 'υ', - 'Φ' => 'φ', - 'Χ' => 'χ', - 'Ψ' => 'ψ', - 'Ω' => 'ω', - 'Ϊ' => 'ϊ', - 'Ϋ' => 'ϋ', - 'Ϗ' => 'ϗ', - 'Ϙ' => 'ϙ', - 'Ϛ' => 'ϛ', - 'Ϝ' => 'ϝ', - 'Ϟ' => 'ϟ', - 'Ϡ' => 'ϡ', - 'Ϣ' => 'ϣ', - 'Ϥ' => 'ϥ', - 'Ϧ' => 'ϧ', - 'Ϩ' => 'ϩ', - 'Ϫ' => 'ϫ', - 'Ϭ' => 'ϭ', - 'Ϯ' => 'ϯ', - 'ϴ' => 'θ', - 'Ϸ' => 'ϸ', - 'Ϲ' => 'ϲ', - 'Ϻ' => 'ϻ', - 'Ͻ' => 'ͻ', - 'Ͼ' => 'ͼ', - 'Ͽ' => 'ͽ', - 'Ѐ' => 'ѐ', - 'Ё' => 'ё', - 'Ђ' => 'ђ', - 'Ѓ' => 'ѓ', - 'Є' => 'є', - 'Ѕ' => 'ѕ', - 'І' => 'і', - 'Ї' => 'ї', - 'Ј' => 'ј', - 'Љ' => 'љ', - 'Њ' => 'њ', - 'Ћ' => 'ћ', - 'Ќ' => 'ќ', - 'Ѝ' => 'ѝ', - 'Ў' => 'ў', - 'Џ' => 'џ', - 'А' => 'а', - 'Б' => 'б', - 'В' => 'в', - 'Г' => 'г', - 'Д' => 'д', - 'Е' => 'е', - 'Ж' => 'ж', - 'З' => 'з', - 'И' => 'и', - 'Й' => 'й', - 'К' => 'к', - 'Л' => 'л', - 'М' => 'м', - 'Н' => 'н', - 'О' => 'о', - 'П' => 'п', - 'Р' => 'р', - 'С' => 'с', - 'Т' => 'т', - 'У' => 'у', - 'Ф' => 'ф', - 'Х' => 'х', - 'Ц' => 'ц', - 'Ч' => 'ч', - 'Ш' => 'ш', - 'Щ' => 'щ', - 'Ъ' => 'ъ', - 'Ы' => 'ы', - 'Ь' => 'ь', - 'Э' => 'э', - 'Ю' => 'ю', - 'Я' => 'я', - 'Ѡ' => 'ѡ', - 'Ѣ' => 'ѣ', - 'Ѥ' => 'ѥ', - 'Ѧ' => 'ѧ', - 'Ѩ' => 'ѩ', - 'Ѫ' => 'ѫ', - 'Ѭ' => 'ѭ', - 'Ѯ' => 'ѯ', - 'Ѱ' => 'ѱ', - 'Ѳ' => 'ѳ', - 'Ѵ' => 'ѵ', - 'Ѷ' => 'ѷ', - 'Ѹ' => 'ѹ', - 'Ѻ' => 'ѻ', - 'Ѽ' => 'ѽ', - 'Ѿ' => 'ѿ', - 'Ҁ' => 'ҁ', - 'Ҋ' => 'ҋ', - 'Ҍ' => 'ҍ', - 'Ҏ' => 'ҏ', - 'Ґ' => 'ґ', - 'Ғ' => 'ғ', - 'Ҕ' => 'ҕ', - 'Җ' => 'җ', - 'Ҙ' => 'ҙ', - 'Қ' => 'қ', - 'Ҝ' => 'ҝ', - 'Ҟ' => 'ҟ', - 'Ҡ' => 'ҡ', - 'Ң' => 'ң', - 'Ҥ' => 'ҥ', - 'Ҧ' => 'ҧ', - 'Ҩ' => 'ҩ', - 'Ҫ' => 'ҫ', - 'Ҭ' => 'ҭ', - 'Ү' => 'ү', - 'Ұ' => 'ұ', - 'Ҳ' => 'ҳ', - 'Ҵ' => 'ҵ', - 'Ҷ' => 'ҷ', - 'Ҹ' => 'ҹ', - 'Һ' => 'һ', - 'Ҽ' => 'ҽ', - 'Ҿ' => 'ҿ', - 'Ӏ' => 'ӏ', - 'Ӂ' => 'ӂ', - 'Ӄ' => 'ӄ', - 'Ӆ' => 'ӆ', - 'Ӈ' => 'ӈ', - 'Ӊ' => 'ӊ', - 'Ӌ' => 'ӌ', - 'Ӎ' => 'ӎ', - 'Ӑ' => 'ӑ', - 'Ӓ' => 'ӓ', - 'Ӕ' => 'ӕ', - 'Ӗ' => 'ӗ', - 'Ә' => 'ә', - 'Ӛ' => 'ӛ', - 'Ӝ' => 'ӝ', - 'Ӟ' => 'ӟ', - 'Ӡ' => 'ӡ', - 'Ӣ' => 'ӣ', - 'Ӥ' => 'ӥ', - 'Ӧ' => 'ӧ', - 'Ө' => 'ө', - 'Ӫ' => 'ӫ', - 'Ӭ' => 'ӭ', - 'Ӯ' => 'ӯ', - 'Ӱ' => 'ӱ', - 'Ӳ' => 'ӳ', - 'Ӵ' => 'ӵ', - 'Ӷ' => 'ӷ', - 'Ӹ' => 'ӹ', - 'Ӻ' => 'ӻ', - 'Ӽ' => 'ӽ', - 'Ӿ' => 'ӿ', - 'Ԁ' => 'ԁ', - 'Ԃ' => 'ԃ', - 'Ԅ' => 'ԅ', - 'Ԇ' => 'ԇ', - 'Ԉ' => 'ԉ', - 'Ԋ' => 'ԋ', - 'Ԍ' => 'ԍ', - 'Ԏ' => 'ԏ', - 'Ԑ' => 'ԑ', - 'Ԓ' => 'ԓ', - 'Ԕ' => 'ԕ', - 'Ԗ' => 'ԗ', - 'Ԙ' => 'ԙ', - 'Ԛ' => 'ԛ', - 'Ԝ' => 'ԝ', - 'Ԟ' => 'ԟ', - 'Ԡ' => 'ԡ', - 'Ԣ' => 'ԣ', - 'Ԥ' => 'ԥ', - 'Ԧ' => 'ԧ', - 'Ԩ' => 'ԩ', - 'Ԫ' => 'ԫ', - 'Ԭ' => 'ԭ', - 'Ԯ' => 'ԯ', - 'Ա' => 'ա', - 'Բ' => 'բ', - 'Գ' => 'գ', - 'Դ' => 'դ', - 'Ե' => 'ե', - 'Զ' => 'զ', - 'Է' => 'է', - 'Ը' => 'ը', - 'Թ' => 'թ', - 'Ժ' => 'ժ', - 'Ի' => 'ի', - 'Լ' => 'լ', - 'Խ' => 'խ', - 'Ծ' => 'ծ', - 'Կ' => 'կ', - 'Հ' => 'հ', - 'Ձ' => 'ձ', - 'Ղ' => 'ղ', - 'Ճ' => 'ճ', - 'Մ' => 'մ', - 'Յ' => 'յ', - 'Ն' => 'ն', - 'Շ' => 'շ', - 'Ո' => 'ո', - 'Չ' => 'չ', - 'Պ' => 'պ', - 'Ջ' => 'ջ', - 'Ռ' => 'ռ', - 'Ս' => 'ս', - 'Վ' => 'վ', - 'Տ' => 'տ', - 'Ր' => 'ր', - 'Ց' => 'ց', - 'Ւ' => 'ւ', - 'Փ' => 'փ', - 'Ք' => 'ք', - 'Օ' => 'օ', - 'Ֆ' => 'ֆ', - 'Ⴀ' => 'ⴀ', - 'Ⴁ' => 'ⴁ', - 'Ⴂ' => 'ⴂ', - 'Ⴃ' => 'ⴃ', - 'Ⴄ' => 'ⴄ', - 'Ⴅ' => 'ⴅ', - 'Ⴆ' => 'ⴆ', - 'Ⴇ' => 'ⴇ', - 'Ⴈ' => 'ⴈ', - 'Ⴉ' => 'ⴉ', - 'Ⴊ' => 'ⴊ', - 'Ⴋ' => 'ⴋ', - 'Ⴌ' => 'ⴌ', - 'Ⴍ' => 'ⴍ', - 'Ⴎ' => 'ⴎ', - 'Ⴏ' => 'ⴏ', - 'Ⴐ' => 'ⴐ', - 'Ⴑ' => 'ⴑ', - 'Ⴒ' => 'ⴒ', - 'Ⴓ' => 'ⴓ', - 'Ⴔ' => 'ⴔ', - 'Ⴕ' => 'ⴕ', - 'Ⴖ' => 'ⴖ', - 'Ⴗ' => 'ⴗ', - 'Ⴘ' => 'ⴘ', - 'Ⴙ' => 'ⴙ', - 'Ⴚ' => 'ⴚ', - 'Ⴛ' => 'ⴛ', - 'Ⴜ' => 'ⴜ', - 'Ⴝ' => 'ⴝ', - 'Ⴞ' => 'ⴞ', - 'Ⴟ' => 'ⴟ', - 'Ⴠ' => 'ⴠ', - 'Ⴡ' => 'ⴡ', - 'Ⴢ' => 'ⴢ', - 'Ⴣ' => 'ⴣ', - 'Ⴤ' => 'ⴤ', - 'Ⴥ' => 'ⴥ', - 'Ⴧ' => 'ⴧ', - 'Ⴭ' => 'ⴭ', - 'Ḁ' => 'ḁ', - 'Ḃ' => 'ḃ', - 'Ḅ' => 'ḅ', - 'Ḇ' => 'ḇ', - 'Ḉ' => 'ḉ', - 'Ḋ' => 'ḋ', - 'Ḍ' => 'ḍ', - 'Ḏ' => 'ḏ', - 'Ḑ' => 'ḑ', - 'Ḓ' => 'ḓ', - 'Ḕ' => 'ḕ', - 'Ḗ' => 'ḗ', - 'Ḙ' => 'ḙ', - 'Ḛ' => 'ḛ', - 'Ḝ' => 'ḝ', - 'Ḟ' => 'ḟ', - 'Ḡ' => 'ḡ', - 'Ḣ' => 'ḣ', - 'Ḥ' => 'ḥ', - 'Ḧ' => 'ḧ', - 'Ḩ' => 'ḩ', - 'Ḫ' => 'ḫ', - 'Ḭ' => 'ḭ', - 'Ḯ' => 'ḯ', - 'Ḱ' => 'ḱ', - 'Ḳ' => 'ḳ', - 'Ḵ' => 'ḵ', - 'Ḷ' => 'ḷ', - 'Ḹ' => 'ḹ', - 'Ḻ' => 'ḻ', - 'Ḽ' => 'ḽ', - 'Ḿ' => 'ḿ', - 'Ṁ' => 'ṁ', - 'Ṃ' => 'ṃ', - 'Ṅ' => 'ṅ', - 'Ṇ' => 'ṇ', - 'Ṉ' => 'ṉ', - 'Ṋ' => 'ṋ', - 'Ṍ' => 'ṍ', - 'Ṏ' => 'ṏ', - 'Ṑ' => 'ṑ', - 'Ṓ' => 'ṓ', - 'Ṕ' => 'ṕ', - 'Ṗ' => 'ṗ', - 'Ṙ' => 'ṙ', - 'Ṛ' => 'ṛ', - 'Ṝ' => 'ṝ', - 'Ṟ' => 'ṟ', - 'Ṡ' => 'ṡ', - 'Ṣ' => 'ṣ', - 'Ṥ' => 'ṥ', - 'Ṧ' => 'ṧ', - 'Ṩ' => 'ṩ', - 'Ṫ' => 'ṫ', - 'Ṭ' => 'ṭ', - 'Ṯ' => 'ṯ', - 'Ṱ' => 'ṱ', - 'Ṳ' => 'ṳ', - 'Ṵ' => 'ṵ', - 'Ṷ' => 'ṷ', - 'Ṹ' => 'ṹ', - 'Ṻ' => 'ṻ', - 'Ṽ' => 'ṽ', - 'Ṿ' => 'ṿ', - 'Ẁ' => 'ẁ', - 'Ẃ' => 'ẃ', - 'Ẅ' => 'ẅ', - 'Ẇ' => 'ẇ', - 'Ẉ' => 'ẉ', - 'Ẋ' => 'ẋ', - 'Ẍ' => 'ẍ', - 'Ẏ' => 'ẏ', - 'Ẑ' => 'ẑ', - 'Ẓ' => 'ẓ', - 'Ẕ' => 'ẕ', - 'ẞ' => 'ß', - 'Ạ' => 'ạ', - 'Ả' => 'ả', - 'Ấ' => 'ấ', - 'Ầ' => 'ầ', - 'Ẩ' => 'ẩ', - 'Ẫ' => 'ẫ', - 'Ậ' => 'ậ', - 'Ắ' => 'ắ', - 'Ằ' => 'ằ', - 'Ẳ' => 'ẳ', - 'Ẵ' => 'ẵ', - 'Ặ' => 'ặ', - 'Ẹ' => 'ẹ', - 'Ẻ' => 'ẻ', - 'Ẽ' => 'ẽ', - 'Ế' => 'ế', - 'Ề' => 'ề', - 'Ể' => 'ể', - 'Ễ' => 'ễ', - 'Ệ' => 'ệ', - 'Ỉ' => 'ỉ', - 'Ị' => 'ị', - 'Ọ' => 'ọ', - 'Ỏ' => 'ỏ', - 'Ố' => 'ố', - 'Ồ' => 'ồ', - 'Ổ' => 'ổ', - 'Ỗ' => 'ỗ', - 'Ộ' => 'ộ', - 'Ớ' => 'ớ', - 'Ờ' => 'ờ', - 'Ở' => 'ở', - 'Ỡ' => 'ỡ', - 'Ợ' => 'ợ', - 'Ụ' => 'ụ', - 'Ủ' => 'ủ', - 'Ứ' => 'ứ', - 'Ừ' => 'ừ', - 'Ử' => 'ử', - 'Ữ' => 'ữ', - 'Ự' => 'ự', - 'Ỳ' => 'ỳ', - 'Ỵ' => 'ỵ', - 'Ỷ' => 'ỷ', - 'Ỹ' => 'ỹ', - 'Ỻ' => 'ỻ', - 'Ỽ' => 'ỽ', - 'Ỿ' => 'ỿ', - 'Ἀ' => 'ἀ', - 'Ἁ' => 'ἁ', - 'Ἂ' => 'ἂ', - 'Ἃ' => 'ἃ', - 'Ἄ' => 'ἄ', - 'Ἅ' => 'ἅ', - 'Ἆ' => 'ἆ', - 'Ἇ' => 'ἇ', - 'Ἐ' => 'ἐ', - 'Ἑ' => 'ἑ', - 'Ἒ' => 'ἒ', - 'Ἓ' => 'ἓ', - 'Ἔ' => 'ἔ', - 'Ἕ' => 'ἕ', - 'Ἠ' => 'ἠ', - 'Ἡ' => 'ἡ', - 'Ἢ' => 'ἢ', - 'Ἣ' => 'ἣ', - 'Ἤ' => 'ἤ', - 'Ἥ' => 'ἥ', - 'Ἦ' => 'ἦ', - 'Ἧ' => 'ἧ', - 'Ἰ' => 'ἰ', - 'Ἱ' => 'ἱ', - 'Ἲ' => 'ἲ', - 'Ἳ' => 'ἳ', - 'Ἴ' => 'ἴ', - 'Ἵ' => 'ἵ', - 'Ἶ' => 'ἶ', - 'Ἷ' => 'ἷ', - 'Ὀ' => 'ὀ', - 'Ὁ' => 'ὁ', - 'Ὂ' => 'ὂ', - 'Ὃ' => 'ὃ', - 'Ὄ' => 'ὄ', - 'Ὅ' => 'ὅ', - 'Ὑ' => 'ὑ', - 'Ὓ' => 'ὓ', - 'Ὕ' => 'ὕ', - 'Ὗ' => 'ὗ', - 'Ὠ' => 'ὠ', - 'Ὡ' => 'ὡ', - 'Ὢ' => 'ὢ', - 'Ὣ' => 'ὣ', - 'Ὤ' => 'ὤ', - 'Ὥ' => 'ὥ', - 'Ὦ' => 'ὦ', - 'Ὧ' => 'ὧ', - 'ᾈ' => 'ᾀ', - 'ᾉ' => 'ᾁ', - 'ᾊ' => 'ᾂ', - 'ᾋ' => 'ᾃ', - 'ᾌ' => 'ᾄ', - 'ᾍ' => 'ᾅ', - 'ᾎ' => 'ᾆ', - 'ᾏ' => 'ᾇ', - 'ᾘ' => 'ᾐ', - 'ᾙ' => 'ᾑ', - 'ᾚ' => 'ᾒ', - 'ᾛ' => 'ᾓ', - 'ᾜ' => 'ᾔ', - 'ᾝ' => 'ᾕ', - 'ᾞ' => 'ᾖ', - 'ᾟ' => 'ᾗ', - 'ᾨ' => 'ᾠ', - 'ᾩ' => 'ᾡ', - 'ᾪ' => 'ᾢ', - 'ᾫ' => 'ᾣ', - 'ᾬ' => 'ᾤ', - 'ᾭ' => 'ᾥ', - 'ᾮ' => 'ᾦ', - 'ᾯ' => 'ᾧ', - 'Ᾰ' => 'ᾰ', - 'Ᾱ' => 'ᾱ', - 'Ὰ' => 'ὰ', - 'Ά' => 'ά', - 'ᾼ' => 'ᾳ', - 'Ὲ' => 'ὲ', - 'Έ' => 'έ', - 'Ὴ' => 'ὴ', - 'Ή' => 'ή', - 'ῌ' => 'ῃ', - 'Ῐ' => 'ῐ', - 'Ῑ' => 'ῑ', - 'Ὶ' => 'ὶ', - 'Ί' => 'ί', - 'Ῠ' => 'ῠ', - 'Ῡ' => 'ῡ', - 'Ὺ' => 'ὺ', - 'Ύ' => 'ύ', - 'Ῥ' => 'ῥ', - 'Ὸ' => 'ὸ', - 'Ό' => 'ό', - 'Ὼ' => 'ὼ', - 'Ώ' => 'ώ', - 'ῼ' => 'ῳ', - 'Ω' => 'ω', - 'K' => 'k', - 'Å' => 'å', - 'Ⅎ' => 'ⅎ', - 'Ⅰ' => 'ⅰ', - 'Ⅱ' => 'ⅱ', - 'Ⅲ' => 'ⅲ', - 'Ⅳ' => 'ⅳ', - 'Ⅴ' => 'ⅴ', - 'Ⅵ' => 'ⅵ', - 'Ⅶ' => 'ⅶ', - 'Ⅷ' => 'ⅷ', - 'Ⅸ' => 'ⅸ', - 'Ⅹ' => 'ⅹ', - 'Ⅺ' => 'ⅺ', - 'Ⅻ' => 'ⅻ', - 'Ⅼ' => 'ⅼ', - 'Ⅽ' => 'ⅽ', - 'Ⅾ' => 'ⅾ', - 'Ⅿ' => 'ⅿ', - 'Ↄ' => 'ↄ', - 'Ⓐ' => 'ⓐ', - 'Ⓑ' => 'ⓑ', - 'Ⓒ' => 'ⓒ', - 'Ⓓ' => 'ⓓ', - 'Ⓔ' => 'ⓔ', - 'Ⓕ' => 'ⓕ', - 'Ⓖ' => 'ⓖ', - 'Ⓗ' => 'ⓗ', - 'Ⓘ' => 'ⓘ', - 'Ⓙ' => 'ⓙ', - 'Ⓚ' => 'ⓚ', - 'Ⓛ' => 'ⓛ', - 'Ⓜ' => 'ⓜ', - 'Ⓝ' => 'ⓝ', - 'Ⓞ' => 'ⓞ', - 'Ⓟ' => 'ⓟ', - 'Ⓠ' => 'ⓠ', - 'Ⓡ' => 'ⓡ', - 'Ⓢ' => 'ⓢ', - 'Ⓣ' => 'ⓣ', - 'Ⓤ' => 'ⓤ', - 'Ⓥ' => 'ⓥ', - 'Ⓦ' => 'ⓦ', - 'Ⓧ' => 'ⓧ', - 'Ⓨ' => 'ⓨ', - 'Ⓩ' => 'ⓩ', - 'Ⰰ' => 'ⰰ', - 'Ⰱ' => 'ⰱ', - 'Ⰲ' => 'ⰲ', - 'Ⰳ' => 'ⰳ', - 'Ⰴ' => 'ⰴ', - 'Ⰵ' => 'ⰵ', - 'Ⰶ' => 'ⰶ', - 'Ⰷ' => 'ⰷ', - 'Ⰸ' => 'ⰸ', - 'Ⰹ' => 'ⰹ', - 'Ⰺ' => 'ⰺ', - 'Ⰻ' => 'ⰻ', - 'Ⰼ' => 'ⰼ', - 'Ⰽ' => 'ⰽ', - 'Ⰾ' => 'ⰾ', - 'Ⰿ' => 'ⰿ', - 'Ⱀ' => 'ⱀ', - 'Ⱁ' => 'ⱁ', - 'Ⱂ' => 'ⱂ', - 'Ⱃ' => 'ⱃ', - 'Ⱄ' => 'ⱄ', - 'Ⱅ' => 'ⱅ', - 'Ⱆ' => 'ⱆ', - 'Ⱇ' => 'ⱇ', - 'Ⱈ' => 'ⱈ', - 'Ⱉ' => 'ⱉ', - 'Ⱊ' => 'ⱊ', - 'Ⱋ' => 'ⱋ', - 'Ⱌ' => 'ⱌ', - 'Ⱍ' => 'ⱍ', - 'Ⱎ' => 'ⱎ', - 'Ⱏ' => 'ⱏ', - 'Ⱐ' => 'ⱐ', - 'Ⱑ' => 'ⱑ', - 'Ⱒ' => 'ⱒ', - 'Ⱓ' => 'ⱓ', - 'Ⱔ' => 'ⱔ', - 'Ⱕ' => 'ⱕ', - 'Ⱖ' => 'ⱖ', - 'Ⱗ' => 'ⱗ', - 'Ⱘ' => 'ⱘ', - 'Ⱙ' => 'ⱙ', - 'Ⱚ' => 'ⱚ', - 'Ⱛ' => 'ⱛ', - 'Ⱜ' => 'ⱜ', - 'Ⱝ' => 'ⱝ', - 'Ⱞ' => 'ⱞ', - 'Ⱡ' => 'ⱡ', - 'Ɫ' => 'ɫ', - 'Ᵽ' => 'ᵽ', - 'Ɽ' => 'ɽ', - 'Ⱨ' => 'ⱨ', - 'Ⱪ' => 'ⱪ', - 'Ⱬ' => 'ⱬ', - 'Ɑ' => 'ɑ', - 'Ɱ' => 'ɱ', - 'Ɐ' => 'ɐ', - 'Ɒ' => 'ɒ', - 'Ⱳ' => 'ⱳ', - 'Ⱶ' => 'ⱶ', - 'Ȿ' => 'ȿ', - 'Ɀ' => 'ɀ', - 'Ⲁ' => 'ⲁ', - 'Ⲃ' => 'ⲃ', - 'Ⲅ' => 'ⲅ', - 'Ⲇ' => 'ⲇ', - 'Ⲉ' => 'ⲉ', - 'Ⲋ' => 'ⲋ', - 'Ⲍ' => 'ⲍ', - 'Ⲏ' => 'ⲏ', - 'Ⲑ' => 'ⲑ', - 'Ⲓ' => 'ⲓ', - 'Ⲕ' => 'ⲕ', - 'Ⲗ' => 'ⲗ', - 'Ⲙ' => 'ⲙ', - 'Ⲛ' => 'ⲛ', - 'Ⲝ' => 'ⲝ', - 'Ⲟ' => 'ⲟ', - 'Ⲡ' => 'ⲡ', - 'Ⲣ' => 'ⲣ', - 'Ⲥ' => 'ⲥ', - 'Ⲧ' => 'ⲧ', - 'Ⲩ' => 'ⲩ', - 'Ⲫ' => 'ⲫ', - 'Ⲭ' => 'ⲭ', - 'Ⲯ' => 'ⲯ', - 'Ⲱ' => 'ⲱ', - 'Ⲳ' => 'ⲳ', - 'Ⲵ' => 'ⲵ', - 'Ⲷ' => 'ⲷ', - 'Ⲹ' => 'ⲹ', - 'Ⲻ' => 'ⲻ', - 'Ⲽ' => 'ⲽ', - 'Ⲿ' => 'ⲿ', - 'Ⳁ' => 'ⳁ', - 'Ⳃ' => 'ⳃ', - 'Ⳅ' => 'ⳅ', - 'Ⳇ' => 'ⳇ', - 'Ⳉ' => 'ⳉ', - 'Ⳋ' => 'ⳋ', - 'Ⳍ' => 'ⳍ', - 'Ⳏ' => 'ⳏ', - 'Ⳑ' => 'ⳑ', - 'Ⳓ' => 'ⳓ', - 'Ⳕ' => 'ⳕ', - 'Ⳗ' => 'ⳗ', - 'Ⳙ' => 'ⳙ', - 'Ⳛ' => 'ⳛ', - 'Ⳝ' => 'ⳝ', - 'Ⳟ' => 'ⳟ', - 'Ⳡ' => 'ⳡ', - 'Ⳣ' => 'ⳣ', - 'Ⳬ' => 'ⳬ', - 'Ⳮ' => 'ⳮ', - 'Ⳳ' => 'ⳳ', - 'Ꙁ' => 'ꙁ', - 'Ꙃ' => 'ꙃ', - 'Ꙅ' => 'ꙅ', - 'Ꙇ' => 'ꙇ', - 'Ꙉ' => 'ꙉ', - 'Ꙋ' => 'ꙋ', - 'Ꙍ' => 'ꙍ', - 'Ꙏ' => 'ꙏ', - 'Ꙑ' => 'ꙑ', - 'Ꙓ' => 'ꙓ', - 'Ꙕ' => 'ꙕ', - 'Ꙗ' => 'ꙗ', - 'Ꙙ' => 'ꙙ', - 'Ꙛ' => 'ꙛ', - 'Ꙝ' => 'ꙝ', - 'Ꙟ' => 'ꙟ', - 'Ꙡ' => 'ꙡ', - 'Ꙣ' => 'ꙣ', - 'Ꙥ' => 'ꙥ', - 'Ꙧ' => 'ꙧ', - 'Ꙩ' => 'ꙩ', - 'Ꙫ' => 'ꙫ', - 'Ꙭ' => 'ꙭ', - 'Ꚁ' => 'ꚁ', - 'Ꚃ' => 'ꚃ', - 'Ꚅ' => 'ꚅ', - 'Ꚇ' => 'ꚇ', - 'Ꚉ' => 'ꚉ', - 'Ꚋ' => 'ꚋ', - 'Ꚍ' => 'ꚍ', - 'Ꚏ' => 'ꚏ', - 'Ꚑ' => 'ꚑ', - 'Ꚓ' => 'ꚓ', - 'Ꚕ' => 'ꚕ', - 'Ꚗ' => 'ꚗ', - 'Ꚙ' => 'ꚙ', - 'Ꚛ' => 'ꚛ', - 'Ꜣ' => 'ꜣ', - 'Ꜥ' => 'ꜥ', - 'Ꜧ' => 'ꜧ', - 'Ꜩ' => 'ꜩ', - 'Ꜫ' => 'ꜫ', - 'Ꜭ' => 'ꜭ', - 'Ꜯ' => 'ꜯ', - 'Ꜳ' => 'ꜳ', - 'Ꜵ' => 'ꜵ', - 'Ꜷ' => 'ꜷ', - 'Ꜹ' => 'ꜹ', - 'Ꜻ' => 'ꜻ', - 'Ꜽ' => 'ꜽ', - 'Ꜿ' => 'ꜿ', - 'Ꝁ' => 'ꝁ', - 'Ꝃ' => 'ꝃ', - 'Ꝅ' => 'ꝅ', - 'Ꝇ' => 'ꝇ', - 'Ꝉ' => 'ꝉ', - 'Ꝋ' => 'ꝋ', - 'Ꝍ' => 'ꝍ', - 'Ꝏ' => 'ꝏ', - 'Ꝑ' => 'ꝑ', - 'Ꝓ' => 'ꝓ', - 'Ꝕ' => 'ꝕ', - 'Ꝗ' => 'ꝗ', - 'Ꝙ' => 'ꝙ', - 'Ꝛ' => 'ꝛ', - 'Ꝝ' => 'ꝝ', - 'Ꝟ' => 'ꝟ', - 'Ꝡ' => 'ꝡ', - 'Ꝣ' => 'ꝣ', - 'Ꝥ' => 'ꝥ', - 'Ꝧ' => 'ꝧ', - 'Ꝩ' => 'ꝩ', - 'Ꝫ' => 'ꝫ', - 'Ꝭ' => 'ꝭ', - 'Ꝯ' => 'ꝯ', - 'Ꝺ' => 'ꝺ', - 'Ꝼ' => 'ꝼ', - 'Ᵹ' => 'ᵹ', - 'Ꝿ' => 'ꝿ', - 'Ꞁ' => 'ꞁ', - 'Ꞃ' => 'ꞃ', - 'Ꞅ' => 'ꞅ', - 'Ꞇ' => 'ꞇ', - 'Ꞌ' => 'ꞌ', - 'Ɥ' => 'ɥ', - 'Ꞑ' => 'ꞑ', - 'Ꞓ' => 'ꞓ', - 'Ꞗ' => 'ꞗ', - 'Ꞙ' => 'ꞙ', - 'Ꞛ' => 'ꞛ', - 'Ꞝ' => 'ꞝ', - 'Ꞟ' => 'ꞟ', - 'Ꞡ' => 'ꞡ', - 'Ꞣ' => 'ꞣ', - 'Ꞥ' => 'ꞥ', - 'Ꞧ' => 'ꞧ', - 'Ꞩ' => 'ꞩ', - 'Ɦ' => 'ɦ', - 'Ɜ' => 'ɜ', - 'Ɡ' => 'ɡ', - 'Ɬ' => 'ɬ', - 'Ʞ' => 'ʞ', - 'Ʇ' => 'ʇ', - 'A' => 'a', - 'B' => 'b', - 'C' => 'c', - 'D' => 'd', - 'E' => 'e', - 'F' => 'f', - 'G' => 'g', - 'H' => 'h', - 'I' => 'i', - 'J' => 'j', - 'K' => 'k', - 'L' => 'l', - 'M' => 'm', - 'N' => 'n', - 'O' => 'o', - 'P' => 'p', - 'Q' => 'q', - 'R' => 'r', - 'S' => 's', - 'T' => 't', - 'U' => 'u', - 'V' => 'v', - 'W' => 'w', - 'X' => 'x', - 'Y' => 'y', - 'Z' => 'z', - '𐐀' => '𐐨', - '𐐁' => '𐐩', - '𐐂' => '𐐪', - '𐐃' => '𐐫', - '𐐄' => '𐐬', - '𐐅' => '𐐭', - '𐐆' => '𐐮', - '𐐇' => '𐐯', - '𐐈' => '𐐰', - '𐐉' => '𐐱', - '𐐊' => '𐐲', - '𐐋' => '𐐳', - '𐐌' => '𐐴', - '𐐍' => '𐐵', - '𐐎' => '𐐶', - '𐐏' => '𐐷', - '𐐐' => '𐐸', - '𐐑' => '𐐹', - '𐐒' => '𐐺', - '𐐓' => '𐐻', - '𐐔' => '𐐼', - '𐐕' => '𐐽', - '𐐖' => '𐐾', - '𐐗' => '𐐿', - '𐐘' => '𐑀', - '𐐙' => '𐑁', - '𐐚' => '𐑂', - '𐐛' => '𐑃', - '𐐜' => '𐑄', - '𐐝' => '𐑅', - '𐐞' => '𐑆', - '𐐟' => '𐑇', - '𐐠' => '𐑈', - '𐐡' => '𐑉', - '𐐢' => '𐑊', - '𐐣' => '𐑋', - '𐐤' => '𐑌', - '𐐥' => '𐑍', - '𐐦' => '𐑎', - '𐐧' => '𐑏', - '𑢠' => '𑣀', - '𑢡' => '𑣁', - '𑢢' => '𑣂', - '𑢣' => '𑣃', - '𑢤' => '𑣄', - '𑢥' => '𑣅', - '𑢦' => '𑣆', - '𑢧' => '𑣇', - '𑢨' => '𑣈', - '𑢩' => '𑣉', - '𑢪' => '𑣊', - '𑢫' => '𑣋', - '𑢬' => '𑣌', - '𑢭' => '𑣍', - '𑢮' => '𑣎', - '𑢯' => '𑣏', - '𑢰' => '𑣐', - '𑢱' => '𑣑', - '𑢲' => '𑣒', - '𑢳' => '𑣓', - '𑢴' => '𑣔', - '𑢵' => '𑣕', - '𑢶' => '𑣖', - '𑢷' => '𑣗', - '𑢸' => '𑣘', - '𑢹' => '𑣙', - '𑢺' => '𑣚', - '𑢻' => '𑣛', - '𑢼' => '𑣜', - '𑢽' => '𑣝', - '𑢾' => '𑣞', - '𑢿' => '𑣟', -); - -$result =& $data; -unset($data); - -return $result; diff --git a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php deleted file mode 100644 index 2a8f6e7..0000000 --- a/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php +++ /dev/null @@ -1,5 +0,0 @@ - 'A', - 'b' => 'B', - 'c' => 'C', - 'd' => 'D', - 'e' => 'E', - 'f' => 'F', - 'g' => 'G', - 'h' => 'H', - 'i' => 'I', - 'j' => 'J', - 'k' => 'K', - 'l' => 'L', - 'm' => 'M', - 'n' => 'N', - 'o' => 'O', - 'p' => 'P', - 'q' => 'Q', - 'r' => 'R', - 's' => 'S', - 't' => 'T', - 'u' => 'U', - 'v' => 'V', - 'w' => 'W', - 'x' => 'X', - 'y' => 'Y', - 'z' => 'Z', - 'µ' => 'Μ', - 'à' => 'À', - 'á' => 'Á', - 'â' => 'Â', - 'ã' => 'Ã', - 'ä' => 'Ä', - 'å' => 'Å', - 'æ' => 'Æ', - 'ç' => 'Ç', - 'è' => 'È', - 'é' => 'É', - 'ê' => 'Ê', - 'ë' => 'Ë', - 'ì' => 'Ì', - 'í' => 'Í', - 'î' => 'Î', - 'ï' => 'Ï', - 'ð' => 'Ð', - 'ñ' => 'Ñ', - 'ò' => 'Ò', - 'ó' => 'Ó', - 'ô' => 'Ô', - 'õ' => 'Õ', - 'ö' => 'Ö', - 'ø' => 'Ø', - 'ù' => 'Ù', - 'ú' => 'Ú', - 'û' => 'Û', - 'ü' => 'Ü', - 'ý' => 'Ý', - 'þ' => 'Þ', - 'ÿ' => 'Ÿ', - 'ā' => 'Ā', - 'ă' => 'Ă', - 'ą' => 'Ą', - 'ć' => 'Ć', - 'ĉ' => 'Ĉ', - 'ċ' => 'Ċ', - 'č' => 'Č', - 'ď' => 'Ď', - 'đ' => 'Đ', - 'ē' => 'Ē', - 'ĕ' => 'Ĕ', - 'ė' => 'Ė', - 'ę' => 'Ę', - 'ě' => 'Ě', - 'ĝ' => 'Ĝ', - 'ğ' => 'Ğ', - 'ġ' => 'Ġ', - 'ģ' => 'Ģ', - 'ĥ' => 'Ĥ', - 'ħ' => 'Ħ', - 'ĩ' => 'Ĩ', - 'ī' => 'Ī', - 'ĭ' => 'Ĭ', - 'į' => 'Į', - 'ı' => 'I', - 'ij' => 'IJ', - 'ĵ' => 'Ĵ', - 'ķ' => 'Ķ', - 'ĺ' => 'Ĺ', - 'ļ' => 'Ļ', - 'ľ' => 'Ľ', - 'ŀ' => 'Ŀ', - 'ł' => 'Ł', - 'ń' => 'Ń', - 'ņ' => 'Ņ', - 'ň' => 'Ň', - 'ŋ' => 'Ŋ', - 'ō' => 'Ō', - 'ŏ' => 'Ŏ', - 'ő' => 'Ő', - 'œ' => 'Œ', - 'ŕ' => 'Ŕ', - 'ŗ' => 'Ŗ', - 'ř' => 'Ř', - 'ś' => 'Ś', - 'ŝ' => 'Ŝ', - 'ş' => 'Ş', - 'š' => 'Š', - 'ţ' => 'Ţ', - 'ť' => 'Ť', - 'ŧ' => 'Ŧ', - 'ũ' => 'Ũ', - 'ū' => 'Ū', - 'ŭ' => 'Ŭ', - 'ů' => 'Ů', - 'ű' => 'Ű', - 'ų' => 'Ų', - 'ŵ' => 'Ŵ', - 'ŷ' => 'Ŷ', - 'ź' => 'Ź', - 'ż' => 'Ż', - 'ž' => 'Ž', - 'ſ' => 'S', - 'ƀ' => 'Ƀ', - 'ƃ' => 'Ƃ', - 'ƅ' => 'Ƅ', - 'ƈ' => 'Ƈ', - 'ƌ' => 'Ƌ', - 'ƒ' => 'Ƒ', - 'ƕ' => 'Ƕ', - 'ƙ' => 'Ƙ', - 'ƚ' => 'Ƚ', - 'ƞ' => 'Ƞ', - 'ơ' => 'Ơ', - 'ƣ' => 'Ƣ', - 'ƥ' => 'Ƥ', - 'ƨ' => 'Ƨ', - 'ƭ' => 'Ƭ', - 'ư' => 'Ư', - 'ƴ' => 'Ƴ', - 'ƶ' => 'Ƶ', - 'ƹ' => 'Ƹ', - 'ƽ' => 'Ƽ', - 'ƿ' => 'Ƿ', - 'Dž' => 'DŽ', - 'dž' => 'DŽ', - 'Lj' => 'LJ', - 'lj' => 'LJ', - 'Nj' => 'NJ', - 'nj' => 'NJ', - 'ǎ' => 'Ǎ', - 'ǐ' => 'Ǐ', - 'ǒ' => 'Ǒ', - 'ǔ' => 'Ǔ', - 'ǖ' => 'Ǖ', - 'ǘ' => 'Ǘ', - 'ǚ' => 'Ǚ', - 'ǜ' => 'Ǜ', - 'ǝ' => 'Ǝ', - 'ǟ' => 'Ǟ', - 'ǡ' => 'Ǡ', - 'ǣ' => 'Ǣ', - 'ǥ' => 'Ǥ', - 'ǧ' => 'Ǧ', - 'ǩ' => 'Ǩ', - 'ǫ' => 'Ǫ', - 'ǭ' => 'Ǭ', - 'ǯ' => 'Ǯ', - 'Dz' => 'DZ', - 'dz' => 'DZ', - 'ǵ' => 'Ǵ', - 'ǹ' => 'Ǹ', - 'ǻ' => 'Ǻ', - 'ǽ' => 'Ǽ', - 'ǿ' => 'Ǿ', - 'ȁ' => 'Ȁ', - 'ȃ' => 'Ȃ', - 'ȅ' => 'Ȅ', - 'ȇ' => 'Ȇ', - 'ȉ' => 'Ȉ', - 'ȋ' => 'Ȋ', - 'ȍ' => 'Ȍ', - 'ȏ' => 'Ȏ', - 'ȑ' => 'Ȑ', - 'ȓ' => 'Ȓ', - 'ȕ' => 'Ȕ', - 'ȗ' => 'Ȗ', - 'ș' => 'Ș', - 'ț' => 'Ț', - 'ȝ' => 'Ȝ', - 'ȟ' => 'Ȟ', - 'ȣ' => 'Ȣ', - 'ȥ' => 'Ȥ', - 'ȧ' => 'Ȧ', - 'ȩ' => 'Ȩ', - 'ȫ' => 'Ȫ', - 'ȭ' => 'Ȭ', - 'ȯ' => 'Ȯ', - 'ȱ' => 'Ȱ', - 'ȳ' => 'Ȳ', - 'ȼ' => 'Ȼ', - 'ȿ' => 'Ȿ', - 'ɀ' => 'Ɀ', - 'ɂ' => 'Ɂ', - 'ɇ' => 'Ɇ', - 'ɉ' => 'Ɉ', - 'ɋ' => 'Ɋ', - 'ɍ' => 'Ɍ', - 'ɏ' => 'Ɏ', - 'ɐ' => 'Ɐ', - 'ɑ' => 'Ɑ', - 'ɒ' => 'Ɒ', - 'ɓ' => 'Ɓ', - 'ɔ' => 'Ɔ', - 'ɖ' => 'Ɖ', - 'ɗ' => 'Ɗ', - 'ə' => 'Ə', - 'ɛ' => 'Ɛ', - 'ɜ' => 'Ɜ', - 'ɠ' => 'Ɠ', - 'ɡ' => 'Ɡ', - 'ɣ' => 'Ɣ', - 'ɥ' => 'Ɥ', - 'ɦ' => 'Ɦ', - 'ɨ' => 'Ɨ', - 'ɩ' => 'Ɩ', - 'ɫ' => 'Ɫ', - 'ɬ' => 'Ɬ', - 'ɯ' => 'Ɯ', - 'ɱ' => 'Ɱ', - 'ɲ' => 'Ɲ', - 'ɵ' => 'Ɵ', - 'ɽ' => 'Ɽ', - 'ʀ' => 'Ʀ', - 'ʃ' => 'Ʃ', - 'ʇ' => 'Ʇ', - 'ʈ' => 'Ʈ', - 'ʉ' => 'Ʉ', - 'ʊ' => 'Ʊ', - 'ʋ' => 'Ʋ', - 'ʌ' => 'Ʌ', - 'ʒ' => 'Ʒ', - 'ʞ' => 'Ʞ', - 'ͅ' => 'Ι', - 'ͱ' => 'Ͱ', - 'ͳ' => 'Ͳ', - 'ͷ' => 'Ͷ', - 'ͻ' => 'Ͻ', - 'ͼ' => 'Ͼ', - 'ͽ' => 'Ͽ', - 'ά' => 'Ά', - 'έ' => 'Έ', - 'ή' => 'Ή', - 'ί' => 'Ί', - 'α' => 'Α', - 'β' => 'Β', - 'γ' => 'Γ', - 'δ' => 'Δ', - 'ε' => 'Ε', - 'ζ' => 'Ζ', - 'η' => 'Η', - 'θ' => 'Θ', - 'ι' => 'Ι', - 'κ' => 'Κ', - 'λ' => 'Λ', - 'μ' => 'Μ', - 'ν' => 'Ν', - 'ξ' => 'Ξ', - 'ο' => 'Ο', - 'π' => 'Π', - 'ρ' => 'Ρ', - 'ς' => 'Σ', - 'σ' => 'Σ', - 'τ' => 'Τ', - 'υ' => 'Υ', - 'φ' => 'Φ', - 'χ' => 'Χ', - 'ψ' => 'Ψ', - 'ω' => 'Ω', - 'ϊ' => 'Ϊ', - 'ϋ' => 'Ϋ', - 'ό' => 'Ό', - 'ύ' => 'Ύ', - 'ώ' => 'Ώ', - 'ϐ' => 'Β', - 'ϑ' => 'Θ', - 'ϕ' => 'Φ', - 'ϖ' => 'Π', - 'ϗ' => 'Ϗ', - 'ϙ' => 'Ϙ', - 'ϛ' => 'Ϛ', - 'ϝ' => 'Ϝ', - 'ϟ' => 'Ϟ', - 'ϡ' => 'Ϡ', - 'ϣ' => 'Ϣ', - 'ϥ' => 'Ϥ', - 'ϧ' => 'Ϧ', - 'ϩ' => 'Ϩ', - 'ϫ' => 'Ϫ', - 'ϭ' => 'Ϭ', - 'ϯ' => 'Ϯ', - 'ϰ' => 'Κ', - 'ϱ' => 'Ρ', - 'ϲ' => 'Ϲ', - 'ϳ' => 'Ϳ', - 'ϵ' => 'Ε', - 'ϸ' => 'Ϸ', - 'ϻ' => 'Ϻ', - 'а' => 'А', - 'б' => 'Б', - 'в' => 'В', - 'г' => 'Г', - 'д' => 'Д', - 'е' => 'Е', - 'ж' => 'Ж', - 'з' => 'З', - 'и' => 'И', - 'й' => 'Й', - 'к' => 'К', - 'л' => 'Л', - 'м' => 'М', - 'н' => 'Н', - 'о' => 'О', - 'п' => 'П', - 'р' => 'Р', - 'с' => 'С', - 'т' => 'Т', - 'у' => 'У', - 'ф' => 'Ф', - 'х' => 'Х', - 'ц' => 'Ц', - 'ч' => 'Ч', - 'ш' => 'Ш', - 'щ' => 'Щ', - 'ъ' => 'Ъ', - 'ы' => 'Ы', - 'ь' => 'Ь', - 'э' => 'Э', - 'ю' => 'Ю', - 'я' => 'Я', - 'ѐ' => 'Ѐ', - 'ё' => 'Ё', - 'ђ' => 'Ђ', - 'ѓ' => 'Ѓ', - 'є' => 'Є', - 'ѕ' => 'Ѕ', - 'і' => 'І', - 'ї' => 'Ї', - 'ј' => 'Ј', - 'љ' => 'Љ', - 'њ' => 'Њ', - 'ћ' => 'Ћ', - 'ќ' => 'Ќ', - 'ѝ' => 'Ѝ', - 'ў' => 'Ў', - 'џ' => 'Џ', - 'ѡ' => 'Ѡ', - 'ѣ' => 'Ѣ', - 'ѥ' => 'Ѥ', - 'ѧ' => 'Ѧ', - 'ѩ' => 'Ѩ', - 'ѫ' => 'Ѫ', - 'ѭ' => 'Ѭ', - 'ѯ' => 'Ѯ', - 'ѱ' => 'Ѱ', - 'ѳ' => 'Ѳ', - 'ѵ' => 'Ѵ', - 'ѷ' => 'Ѷ', - 'ѹ' => 'Ѹ', - 'ѻ' => 'Ѻ', - 'ѽ' => 'Ѽ', - 'ѿ' => 'Ѿ', - 'ҁ' => 'Ҁ', - 'ҋ' => 'Ҋ', - 'ҍ' => 'Ҍ', - 'ҏ' => 'Ҏ', - 'ґ' => 'Ґ', - 'ғ' => 'Ғ', - 'ҕ' => 'Ҕ', - 'җ' => 'Җ', - 'ҙ' => 'Ҙ', - 'қ' => 'Қ', - 'ҝ' => 'Ҝ', - 'ҟ' => 'Ҟ', - 'ҡ' => 'Ҡ', - 'ң' => 'Ң', - 'ҥ' => 'Ҥ', - 'ҧ' => 'Ҧ', - 'ҩ' => 'Ҩ', - 'ҫ' => 'Ҫ', - 'ҭ' => 'Ҭ', - 'ү' => 'Ү', - 'ұ' => 'Ұ', - 'ҳ' => 'Ҳ', - 'ҵ' => 'Ҵ', - 'ҷ' => 'Ҷ', - 'ҹ' => 'Ҹ', - 'һ' => 'Һ', - 'ҽ' => 'Ҽ', - 'ҿ' => 'Ҿ', - 'ӂ' => 'Ӂ', - 'ӄ' => 'Ӄ', - 'ӆ' => 'Ӆ', - 'ӈ' => 'Ӈ', - 'ӊ' => 'Ӊ', - 'ӌ' => 'Ӌ', - 'ӎ' => 'Ӎ', - 'ӏ' => 'Ӏ', - 'ӑ' => 'Ӑ', - 'ӓ' => 'Ӓ', - 'ӕ' => 'Ӕ', - 'ӗ' => 'Ӗ', - 'ә' => 'Ә', - 'ӛ' => 'Ӛ', - 'ӝ' => 'Ӝ', - 'ӟ' => 'Ӟ', - 'ӡ' => 'Ӡ', - 'ӣ' => 'Ӣ', - 'ӥ' => 'Ӥ', - 'ӧ' => 'Ӧ', - 'ө' => 'Ө', - 'ӫ' => 'Ӫ', - 'ӭ' => 'Ӭ', - 'ӯ' => 'Ӯ', - 'ӱ' => 'Ӱ', - 'ӳ' => 'Ӳ', - 'ӵ' => 'Ӵ', - 'ӷ' => 'Ӷ', - 'ӹ' => 'Ӹ', - 'ӻ' => 'Ӻ', - 'ӽ' => 'Ӽ', - 'ӿ' => 'Ӿ', - 'ԁ' => 'Ԁ', - 'ԃ' => 'Ԃ', - 'ԅ' => 'Ԅ', - 'ԇ' => 'Ԇ', - 'ԉ' => 'Ԉ', - 'ԋ' => 'Ԋ', - 'ԍ' => 'Ԍ', - 'ԏ' => 'Ԏ', - 'ԑ' => 'Ԑ', - 'ԓ' => 'Ԓ', - 'ԕ' => 'Ԕ', - 'ԗ' => 'Ԗ', - 'ԙ' => 'Ԙ', - 'ԛ' => 'Ԛ', - 'ԝ' => 'Ԝ', - 'ԟ' => 'Ԟ', - 'ԡ' => 'Ԡ', - 'ԣ' => 'Ԣ', - 'ԥ' => 'Ԥ', - 'ԧ' => 'Ԧ', - 'ԩ' => 'Ԩ', - 'ԫ' => 'Ԫ', - 'ԭ' => 'Ԭ', - 'ԯ' => 'Ԯ', - 'ա' => 'Ա', - 'բ' => 'Բ', - 'գ' => 'Գ', - 'դ' => 'Դ', - 'ե' => 'Ե', - 'զ' => 'Զ', - 'է' => 'Է', - 'ը' => 'Ը', - 'թ' => 'Թ', - 'ժ' => 'Ժ', - 'ի' => 'Ի', - 'լ' => 'Լ', - 'խ' => 'Խ', - 'ծ' => 'Ծ', - 'կ' => 'Կ', - 'հ' => 'Հ', - 'ձ' => 'Ձ', - 'ղ' => 'Ղ', - 'ճ' => 'Ճ', - 'մ' => 'Մ', - 'յ' => 'Յ', - 'ն' => 'Ն', - 'շ' => 'Շ', - 'ո' => 'Ո', - 'չ' => 'Չ', - 'պ' => 'Պ', - 'ջ' => 'Ջ', - 'ռ' => 'Ռ', - 'ս' => 'Ս', - 'վ' => 'Վ', - 'տ' => 'Տ', - 'ր' => 'Ր', - 'ց' => 'Ց', - 'ւ' => 'Ւ', - 'փ' => 'Փ', - 'ք' => 'Ք', - 'օ' => 'Օ', - 'ֆ' => 'Ֆ', - 'ᵹ' => 'Ᵹ', - 'ᵽ' => 'Ᵽ', - 'ḁ' => 'Ḁ', - 'ḃ' => 'Ḃ', - 'ḅ' => 'Ḅ', - 'ḇ' => 'Ḇ', - 'ḉ' => 'Ḉ', - 'ḋ' => 'Ḋ', - 'ḍ' => 'Ḍ', - 'ḏ' => 'Ḏ', - 'ḑ' => 'Ḑ', - 'ḓ' => 'Ḓ', - 'ḕ' => 'Ḕ', - 'ḗ' => 'Ḗ', - 'ḙ' => 'Ḙ', - 'ḛ' => 'Ḛ', - 'ḝ' => 'Ḝ', - 'ḟ' => 'Ḟ', - 'ḡ' => 'Ḡ', - 'ḣ' => 'Ḣ', - 'ḥ' => 'Ḥ', - 'ḧ' => 'Ḧ', - 'ḩ' => 'Ḩ', - 'ḫ' => 'Ḫ', - 'ḭ' => 'Ḭ', - 'ḯ' => 'Ḯ', - 'ḱ' => 'Ḱ', - 'ḳ' => 'Ḳ', - 'ḵ' => 'Ḵ', - 'ḷ' => 'Ḷ', - 'ḹ' => 'Ḹ', - 'ḻ' => 'Ḻ', - 'ḽ' => 'Ḽ', - 'ḿ' => 'Ḿ', - 'ṁ' => 'Ṁ', - 'ṃ' => 'Ṃ', - 'ṅ' => 'Ṅ', - 'ṇ' => 'Ṇ', - 'ṉ' => 'Ṉ', - 'ṋ' => 'Ṋ', - 'ṍ' => 'Ṍ', - 'ṏ' => 'Ṏ', - 'ṑ' => 'Ṑ', - 'ṓ' => 'Ṓ', - 'ṕ' => 'Ṕ', - 'ṗ' => 'Ṗ', - 'ṙ' => 'Ṙ', - 'ṛ' => 'Ṛ', - 'ṝ' => 'Ṝ', - 'ṟ' => 'Ṟ', - 'ṡ' => 'Ṡ', - 'ṣ' => 'Ṣ', - 'ṥ' => 'Ṥ', - 'ṧ' => 'Ṧ', - 'ṩ' => 'Ṩ', - 'ṫ' => 'Ṫ', - 'ṭ' => 'Ṭ', - 'ṯ' => 'Ṯ', - 'ṱ' => 'Ṱ', - 'ṳ' => 'Ṳ', - 'ṵ' => 'Ṵ', - 'ṷ' => 'Ṷ', - 'ṹ' => 'Ṹ', - 'ṻ' => 'Ṻ', - 'ṽ' => 'Ṽ', - 'ṿ' => 'Ṿ', - 'ẁ' => 'Ẁ', - 'ẃ' => 'Ẃ', - 'ẅ' => 'Ẅ', - 'ẇ' => 'Ẇ', - 'ẉ' => 'Ẉ', - 'ẋ' => 'Ẋ', - 'ẍ' => 'Ẍ', - 'ẏ' => 'Ẏ', - 'ẑ' => 'Ẑ', - 'ẓ' => 'Ẓ', - 'ẕ' => 'Ẕ', - 'ẛ' => 'Ṡ', - 'ạ' => 'Ạ', - 'ả' => 'Ả', - 'ấ' => 'Ấ', - 'ầ' => 'Ầ', - 'ẩ' => 'Ẩ', - 'ẫ' => 'Ẫ', - 'ậ' => 'Ậ', - 'ắ' => 'Ắ', - 'ằ' => 'Ằ', - 'ẳ' => 'Ẳ', - 'ẵ' => 'Ẵ', - 'ặ' => 'Ặ', - 'ẹ' => 'Ẹ', - 'ẻ' => 'Ẻ', - 'ẽ' => 'Ẽ', - 'ế' => 'Ế', - 'ề' => 'Ề', - 'ể' => 'Ể', - 'ễ' => 'Ễ', - 'ệ' => 'Ệ', - 'ỉ' => 'Ỉ', - 'ị' => 'Ị', - 'ọ' => 'Ọ', - 'ỏ' => 'Ỏ', - 'ố' => 'Ố', - 'ồ' => 'Ồ', - 'ổ' => 'Ổ', - 'ỗ' => 'Ỗ', - 'ộ' => 'Ộ', - 'ớ' => 'Ớ', - 'ờ' => 'Ờ', - 'ở' => 'Ở', - 'ỡ' => 'Ỡ', - 'ợ' => 'Ợ', - 'ụ' => 'Ụ', - 'ủ' => 'Ủ', - 'ứ' => 'Ứ', - 'ừ' => 'Ừ', - 'ử' => 'Ử', - 'ữ' => 'Ữ', - 'ự' => 'Ự', - 'ỳ' => 'Ỳ', - 'ỵ' => 'Ỵ', - 'ỷ' => 'Ỷ', - 'ỹ' => 'Ỹ', - 'ỻ' => 'Ỻ', - 'ỽ' => 'Ỽ', - 'ỿ' => 'Ỿ', - 'ἀ' => 'Ἀ', - 'ἁ' => 'Ἁ', - 'ἂ' => 'Ἂ', - 'ἃ' => 'Ἃ', - 'ἄ' => 'Ἄ', - 'ἅ' => 'Ἅ', - 'ἆ' => 'Ἆ', - 'ἇ' => 'Ἇ', - 'ἐ' => 'Ἐ', - 'ἑ' => 'Ἑ', - 'ἒ' => 'Ἒ', - 'ἓ' => 'Ἓ', - 'ἔ' => 'Ἔ', - 'ἕ' => 'Ἕ', - 'ἠ' => 'Ἠ', - 'ἡ' => 'Ἡ', - 'ἢ' => 'Ἢ', - 'ἣ' => 'Ἣ', - 'ἤ' => 'Ἤ', - 'ἥ' => 'Ἥ', - 'ἦ' => 'Ἦ', - 'ἧ' => 'Ἧ', - 'ἰ' => 'Ἰ', - 'ἱ' => 'Ἱ', - 'ἲ' => 'Ἲ', - 'ἳ' => 'Ἳ', - 'ἴ' => 'Ἴ', - 'ἵ' => 'Ἵ', - 'ἶ' => 'Ἶ', - 'ἷ' => 'Ἷ', - 'ὀ' => 'Ὀ', - 'ὁ' => 'Ὁ', - 'ὂ' => 'Ὂ', - 'ὃ' => 'Ὃ', - 'ὄ' => 'Ὄ', - 'ὅ' => 'Ὅ', - 'ὑ' => 'Ὑ', - 'ὓ' => 'Ὓ', - 'ὕ' => 'Ὕ', - 'ὗ' => 'Ὗ', - 'ὠ' => 'Ὠ', - 'ὡ' => 'Ὡ', - 'ὢ' => 'Ὢ', - 'ὣ' => 'Ὣ', - 'ὤ' => 'Ὤ', - 'ὥ' => 'Ὥ', - 'ὦ' => 'Ὦ', - 'ὧ' => 'Ὧ', - 'ὰ' => 'Ὰ', - 'ά' => 'Ά', - 'ὲ' => 'Ὲ', - 'έ' => 'Έ', - 'ὴ' => 'Ὴ', - 'ή' => 'Ή', - 'ὶ' => 'Ὶ', - 'ί' => 'Ί', - 'ὸ' => 'Ὸ', - 'ό' => 'Ό', - 'ὺ' => 'Ὺ', - 'ύ' => 'Ύ', - 'ὼ' => 'Ὼ', - 'ώ' => 'Ώ', - 'ᾀ' => 'ᾈ', - 'ᾁ' => 'ᾉ', - 'ᾂ' => 'ᾊ', - 'ᾃ' => 'ᾋ', - 'ᾄ' => 'ᾌ', - 'ᾅ' => 'ᾍ', - 'ᾆ' => 'ᾎ', - 'ᾇ' => 'ᾏ', - 'ᾐ' => 'ᾘ', - 'ᾑ' => 'ᾙ', - 'ᾒ' => 'ᾚ', - 'ᾓ' => 'ᾛ', - 'ᾔ' => 'ᾜ', - 'ᾕ' => 'ᾝ', - 'ᾖ' => 'ᾞ', - 'ᾗ' => 'ᾟ', - 'ᾠ' => 'ᾨ', - 'ᾡ' => 'ᾩ', - 'ᾢ' => 'ᾪ', - 'ᾣ' => 'ᾫ', - 'ᾤ' => 'ᾬ', - 'ᾥ' => 'ᾭ', - 'ᾦ' => 'ᾮ', - 'ᾧ' => 'ᾯ', - 'ᾰ' => 'Ᾰ', - 'ᾱ' => 'Ᾱ', - 'ᾳ' => 'ᾼ', - 'ι' => 'Ι', - 'ῃ' => 'ῌ', - 'ῐ' => 'Ῐ', - 'ῑ' => 'Ῑ', - 'ῠ' => 'Ῠ', - 'ῡ' => 'Ῡ', - 'ῥ' => 'Ῥ', - 'ῳ' => 'ῼ', - 'ⅎ' => 'Ⅎ', - 'ⅰ' => 'Ⅰ', - 'ⅱ' => 'Ⅱ', - 'ⅲ' => 'Ⅲ', - 'ⅳ' => 'Ⅳ', - 'ⅴ' => 'Ⅴ', - 'ⅵ' => 'Ⅵ', - 'ⅶ' => 'Ⅶ', - 'ⅷ' => 'Ⅷ', - 'ⅸ' => 'Ⅸ', - 'ⅹ' => 'Ⅹ', - 'ⅺ' => 'Ⅺ', - 'ⅻ' => 'Ⅻ', - 'ⅼ' => 'Ⅼ', - 'ⅽ' => 'Ⅽ', - 'ⅾ' => 'Ⅾ', - 'ⅿ' => 'Ⅿ', - 'ↄ' => 'Ↄ', - 'ⓐ' => 'Ⓐ', - 'ⓑ' => 'Ⓑ', - 'ⓒ' => 'Ⓒ', - 'ⓓ' => 'Ⓓ', - 'ⓔ' => 'Ⓔ', - 'ⓕ' => 'Ⓕ', - 'ⓖ' => 'Ⓖ', - 'ⓗ' => 'Ⓗ', - 'ⓘ' => 'Ⓘ', - 'ⓙ' => 'Ⓙ', - 'ⓚ' => 'Ⓚ', - 'ⓛ' => 'Ⓛ', - 'ⓜ' => 'Ⓜ', - 'ⓝ' => 'Ⓝ', - 'ⓞ' => 'Ⓞ', - 'ⓟ' => 'Ⓟ', - 'ⓠ' => 'Ⓠ', - 'ⓡ' => 'Ⓡ', - 'ⓢ' => 'Ⓢ', - 'ⓣ' => 'Ⓣ', - 'ⓤ' => 'Ⓤ', - 'ⓥ' => 'Ⓥ', - 'ⓦ' => 'Ⓦ', - 'ⓧ' => 'Ⓧ', - 'ⓨ' => 'Ⓨ', - 'ⓩ' => 'Ⓩ', - 'ⰰ' => 'Ⰰ', - 'ⰱ' => 'Ⰱ', - 'ⰲ' => 'Ⰲ', - 'ⰳ' => 'Ⰳ', - 'ⰴ' => 'Ⰴ', - 'ⰵ' => 'Ⰵ', - 'ⰶ' => 'Ⰶ', - 'ⰷ' => 'Ⰷ', - 'ⰸ' => 'Ⰸ', - 'ⰹ' => 'Ⰹ', - 'ⰺ' => 'Ⰺ', - 'ⰻ' => 'Ⰻ', - 'ⰼ' => 'Ⰼ', - 'ⰽ' => 'Ⰽ', - 'ⰾ' => 'Ⰾ', - 'ⰿ' => 'Ⰿ', - 'ⱀ' => 'Ⱀ', - 'ⱁ' => 'Ⱁ', - 'ⱂ' => 'Ⱂ', - 'ⱃ' => 'Ⱃ', - 'ⱄ' => 'Ⱄ', - 'ⱅ' => 'Ⱅ', - 'ⱆ' => 'Ⱆ', - 'ⱇ' => 'Ⱇ', - 'ⱈ' => 'Ⱈ', - 'ⱉ' => 'Ⱉ', - 'ⱊ' => 'Ⱊ', - 'ⱋ' => 'Ⱋ', - 'ⱌ' => 'Ⱌ', - 'ⱍ' => 'Ⱍ', - 'ⱎ' => 'Ⱎ', - 'ⱏ' => 'Ⱏ', - 'ⱐ' => 'Ⱐ', - 'ⱑ' => 'Ⱑ', - 'ⱒ' => 'Ⱒ', - 'ⱓ' => 'Ⱓ', - 'ⱔ' => 'Ⱔ', - 'ⱕ' => 'Ⱕ', - 'ⱖ' => 'Ⱖ', - 'ⱗ' => 'Ⱗ', - 'ⱘ' => 'Ⱘ', - 'ⱙ' => 'Ⱙ', - 'ⱚ' => 'Ⱚ', - 'ⱛ' => 'Ⱛ', - 'ⱜ' => 'Ⱜ', - 'ⱝ' => 'Ⱝ', - 'ⱞ' => 'Ⱞ', - 'ⱡ' => 'Ⱡ', - 'ⱥ' => 'Ⱥ', - 'ⱦ' => 'Ⱦ', - 'ⱨ' => 'Ⱨ', - 'ⱪ' => 'Ⱪ', - 'ⱬ' => 'Ⱬ', - 'ⱳ' => 'Ⱳ', - 'ⱶ' => 'Ⱶ', - 'ⲁ' => 'Ⲁ', - 'ⲃ' => 'Ⲃ', - 'ⲅ' => 'Ⲅ', - 'ⲇ' => 'Ⲇ', - 'ⲉ' => 'Ⲉ', - 'ⲋ' => 'Ⲋ', - 'ⲍ' => 'Ⲍ', - 'ⲏ' => 'Ⲏ', - 'ⲑ' => 'Ⲑ', - 'ⲓ' => 'Ⲓ', - 'ⲕ' => 'Ⲕ', - 'ⲗ' => 'Ⲗ', - 'ⲙ' => 'Ⲙ', - 'ⲛ' => 'Ⲛ', - 'ⲝ' => 'Ⲝ', - 'ⲟ' => 'Ⲟ', - 'ⲡ' => 'Ⲡ', - 'ⲣ' => 'Ⲣ', - 'ⲥ' => 'Ⲥ', - 'ⲧ' => 'Ⲧ', - 'ⲩ' => 'Ⲩ', - 'ⲫ' => 'Ⲫ', - 'ⲭ' => 'Ⲭ', - 'ⲯ' => 'Ⲯ', - 'ⲱ' => 'Ⲱ', - 'ⲳ' => 'Ⲳ', - 'ⲵ' => 'Ⲵ', - 'ⲷ' => 'Ⲷ', - 'ⲹ' => 'Ⲹ', - 'ⲻ' => 'Ⲻ', - 'ⲽ' => 'Ⲽ', - 'ⲿ' => 'Ⲿ', - 'ⳁ' => 'Ⳁ', - 'ⳃ' => 'Ⳃ', - 'ⳅ' => 'Ⳅ', - 'ⳇ' => 'Ⳇ', - 'ⳉ' => 'Ⳉ', - 'ⳋ' => 'Ⳋ', - 'ⳍ' => 'Ⳍ', - 'ⳏ' => 'Ⳏ', - 'ⳑ' => 'Ⳑ', - 'ⳓ' => 'Ⳓ', - 'ⳕ' => 'Ⳕ', - 'ⳗ' => 'Ⳗ', - 'ⳙ' => 'Ⳙ', - 'ⳛ' => 'Ⳛ', - 'ⳝ' => 'Ⳝ', - 'ⳟ' => 'Ⳟ', - 'ⳡ' => 'Ⳡ', - 'ⳣ' => 'Ⳣ', - 'ⳬ' => 'Ⳬ', - 'ⳮ' => 'Ⳮ', - 'ⳳ' => 'Ⳳ', - 'ⴀ' => 'Ⴀ', - 'ⴁ' => 'Ⴁ', - 'ⴂ' => 'Ⴂ', - 'ⴃ' => 'Ⴃ', - 'ⴄ' => 'Ⴄ', - 'ⴅ' => 'Ⴅ', - 'ⴆ' => 'Ⴆ', - 'ⴇ' => 'Ⴇ', - 'ⴈ' => 'Ⴈ', - 'ⴉ' => 'Ⴉ', - 'ⴊ' => 'Ⴊ', - 'ⴋ' => 'Ⴋ', - 'ⴌ' => 'Ⴌ', - 'ⴍ' => 'Ⴍ', - 'ⴎ' => 'Ⴎ', - 'ⴏ' => 'Ⴏ', - 'ⴐ' => 'Ⴐ', - 'ⴑ' => 'Ⴑ', - 'ⴒ' => 'Ⴒ', - 'ⴓ' => 'Ⴓ', - 'ⴔ' => 'Ⴔ', - 'ⴕ' => 'Ⴕ', - 'ⴖ' => 'Ⴖ', - 'ⴗ' => 'Ⴗ', - 'ⴘ' => 'Ⴘ', - 'ⴙ' => 'Ⴙ', - 'ⴚ' => 'Ⴚ', - 'ⴛ' => 'Ⴛ', - 'ⴜ' => 'Ⴜ', - 'ⴝ' => 'Ⴝ', - 'ⴞ' => 'Ⴞ', - 'ⴟ' => 'Ⴟ', - 'ⴠ' => 'Ⴠ', - 'ⴡ' => 'Ⴡ', - 'ⴢ' => 'Ⴢ', - 'ⴣ' => 'Ⴣ', - 'ⴤ' => 'Ⴤ', - 'ⴥ' => 'Ⴥ', - 'ⴧ' => 'Ⴧ', - 'ⴭ' => 'Ⴭ', - 'ꙁ' => 'Ꙁ', - 'ꙃ' => 'Ꙃ', - 'ꙅ' => 'Ꙅ', - 'ꙇ' => 'Ꙇ', - 'ꙉ' => 'Ꙉ', - 'ꙋ' => 'Ꙋ', - 'ꙍ' => 'Ꙍ', - 'ꙏ' => 'Ꙏ', - 'ꙑ' => 'Ꙑ', - 'ꙓ' => 'Ꙓ', - 'ꙕ' => 'Ꙕ', - 'ꙗ' => 'Ꙗ', - 'ꙙ' => 'Ꙙ', - 'ꙛ' => 'Ꙛ', - 'ꙝ' => 'Ꙝ', - 'ꙟ' => 'Ꙟ', - 'ꙡ' => 'Ꙡ', - 'ꙣ' => 'Ꙣ', - 'ꙥ' => 'Ꙥ', - 'ꙧ' => 'Ꙧ', - 'ꙩ' => 'Ꙩ', - 'ꙫ' => 'Ꙫ', - 'ꙭ' => 'Ꙭ', - 'ꚁ' => 'Ꚁ', - 'ꚃ' => 'Ꚃ', - 'ꚅ' => 'Ꚅ', - 'ꚇ' => 'Ꚇ', - 'ꚉ' => 'Ꚉ', - 'ꚋ' => 'Ꚋ', - 'ꚍ' => 'Ꚍ', - 'ꚏ' => 'Ꚏ', - 'ꚑ' => 'Ꚑ', - 'ꚓ' => 'Ꚓ', - 'ꚕ' => 'Ꚕ', - 'ꚗ' => 'Ꚗ', - 'ꚙ' => 'Ꚙ', - 'ꚛ' => 'Ꚛ', - 'ꜣ' => 'Ꜣ', - 'ꜥ' => 'Ꜥ', - 'ꜧ' => 'Ꜧ', - 'ꜩ' => 'Ꜩ', - 'ꜫ' => 'Ꜫ', - 'ꜭ' => 'Ꜭ', - 'ꜯ' => 'Ꜯ', - 'ꜳ' => 'Ꜳ', - 'ꜵ' => 'Ꜵ', - 'ꜷ' => 'Ꜷ', - 'ꜹ' => 'Ꜹ', - 'ꜻ' => 'Ꜻ', - 'ꜽ' => 'Ꜽ', - 'ꜿ' => 'Ꜿ', - 'ꝁ' => 'Ꝁ', - 'ꝃ' => 'Ꝃ', - 'ꝅ' => 'Ꝅ', - 'ꝇ' => 'Ꝇ', - 'ꝉ' => 'Ꝉ', - 'ꝋ' => 'Ꝋ', - 'ꝍ' => 'Ꝍ', - 'ꝏ' => 'Ꝏ', - 'ꝑ' => 'Ꝑ', - 'ꝓ' => 'Ꝓ', - 'ꝕ' => 'Ꝕ', - 'ꝗ' => 'Ꝗ', - 'ꝙ' => 'Ꝙ', - 'ꝛ' => 'Ꝛ', - 'ꝝ' => 'Ꝝ', - 'ꝟ' => 'Ꝟ', - 'ꝡ' => 'Ꝡ', - 'ꝣ' => 'Ꝣ', - 'ꝥ' => 'Ꝥ', - 'ꝧ' => 'Ꝧ', - 'ꝩ' => 'Ꝩ', - 'ꝫ' => 'Ꝫ', - 'ꝭ' => 'Ꝭ', - 'ꝯ' => 'Ꝯ', - 'ꝺ' => 'Ꝺ', - 'ꝼ' => 'Ꝼ', - 'ꝿ' => 'Ꝿ', - 'ꞁ' => 'Ꞁ', - 'ꞃ' => 'Ꞃ', - 'ꞅ' => 'Ꞅ', - 'ꞇ' => 'Ꞇ', - 'ꞌ' => 'Ꞌ', - 'ꞑ' => 'Ꞑ', - 'ꞓ' => 'Ꞓ', - 'ꞗ' => 'Ꞗ', - 'ꞙ' => 'Ꞙ', - 'ꞛ' => 'Ꞛ', - 'ꞝ' => 'Ꞝ', - 'ꞟ' => 'Ꞟ', - 'ꞡ' => 'Ꞡ', - 'ꞣ' => 'Ꞣ', - 'ꞥ' => 'Ꞥ', - 'ꞧ' => 'Ꞧ', - 'ꞩ' => 'Ꞩ', - 'a' => 'A', - 'b' => 'B', - 'c' => 'C', - 'd' => 'D', - 'e' => 'E', - 'f' => 'F', - 'g' => 'G', - 'h' => 'H', - 'i' => 'I', - 'j' => 'J', - 'k' => 'K', - 'l' => 'L', - 'm' => 'M', - 'n' => 'N', - 'o' => 'O', - 'p' => 'P', - 'q' => 'Q', - 'r' => 'R', - 's' => 'S', - 't' => 'T', - 'u' => 'U', - 'v' => 'V', - 'w' => 'W', - 'x' => 'X', - 'y' => 'Y', - 'z' => 'Z', - '𐐨' => '𐐀', - '𐐩' => '𐐁', - '𐐪' => '𐐂', - '𐐫' => '𐐃', - '𐐬' => '𐐄', - '𐐭' => '𐐅', - '𐐮' => '𐐆', - '𐐯' => '𐐇', - '𐐰' => '𐐈', - '𐐱' => '𐐉', - '𐐲' => '𐐊', - '𐐳' => '𐐋', - '𐐴' => '𐐌', - '𐐵' => '𐐍', - '𐐶' => '𐐎', - '𐐷' => '𐐏', - '𐐸' => '𐐐', - '𐐹' => '𐐑', - '𐐺' => '𐐒', - '𐐻' => '𐐓', - '𐐼' => '𐐔', - '𐐽' => '𐐕', - '𐐾' => '𐐖', - '𐐿' => '𐐗', - '𐑀' => '𐐘', - '𐑁' => '𐐙', - '𐑂' => '𐐚', - '𐑃' => '𐐛', - '𐑄' => '𐐜', - '𐑅' => '𐐝', - '𐑆' => '𐐞', - '𐑇' => '𐐟', - '𐑈' => '𐐠', - '𐑉' => '𐐡', - '𐑊' => '𐐢', - '𐑋' => '𐐣', - '𐑌' => '𐐤', - '𐑍' => '𐐥', - '𐑎' => '𐐦', - '𐑏' => '𐐧', - '𑣀' => '𑢠', - '𑣁' => '𑢡', - '𑣂' => '𑢢', - '𑣃' => '𑢣', - '𑣄' => '𑢤', - '𑣅' => '𑢥', - '𑣆' => '𑢦', - '𑣇' => '𑢧', - '𑣈' => '𑢨', - '𑣉' => '𑢩', - '𑣊' => '𑢪', - '𑣋' => '𑢫', - '𑣌' => '𑢬', - '𑣍' => '𑢭', - '𑣎' => '𑢮', - '𑣏' => '𑢯', - '𑣐' => '𑢰', - '𑣑' => '𑢱', - '𑣒' => '𑢲', - '𑣓' => '𑢳', - '𑣔' => '𑢴', - '𑣕' => '𑢵', - '𑣖' => '𑢶', - '𑣗' => '𑢷', - '𑣘' => '𑢸', - '𑣙' => '𑢹', - '𑣚' => '𑢺', - '𑣛' => '𑢻', - '𑣜' => '𑢼', - '𑣝' => '𑢽', - '𑣞' => '𑢾', - '𑣟' => '𑢿', -); - -$result =& $data; -unset($data); - -return $result; diff --git a/vendor/symfony/polyfill-mbstring/bootstrap.php b/vendor/symfony/polyfill-mbstring/bootstrap.php deleted file mode 100644 index 2fdcc5a..0000000 --- a/vendor/symfony/polyfill-mbstring/bootstrap.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Polyfill\Mbstring as p; - -if (!function_exists('mb_strlen')) { - define('MB_CASE_UPPER', 0); - define('MB_CASE_LOWER', 1); - define('MB_CASE_TITLE', 2); - - function mb_convert_encoding($s, $to, $from = null) { return p\Mbstring::mb_convert_encoding($s, $to, $from); } - function mb_decode_mimeheader($s) { return p\Mbstring::mb_decode_mimeheader($s); } - function mb_encode_mimeheader($s, $charset = null, $transferEnc = null, $lf = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($s, $charset, $transferEnc, $lf, $indent); } - function mb_decode_numericentity($s, $convmap, $enc = null) { return p\Mbstring::mb_decode_numericentity($s, $convmap, $enc); } - function mb_encode_numericentity($s, $convmap, $enc = null, $is_hex = false) { return p\Mbstring::mb_encode_numericentity($s, $convmap, $enc, $is_hex); } - function mb_convert_case($s, $mode, $enc = null) { return p\Mbstring::mb_convert_case($s, $mode, $enc); } - function mb_internal_encoding($enc = null) { return p\Mbstring::mb_internal_encoding($enc); } - function mb_language($lang = null) { return p\Mbstring::mb_language($lang); } - function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } - function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } - function mb_check_encoding($var = null, $encoding = null) { return p\Mbstring::mb_check_encoding($var, $encoding); } - function mb_detect_encoding($str, $encodingList = null, $strict = false) { return p\Mbstring::mb_detect_encoding($str, $encodingList, $strict); } - function mb_detect_order($encodingList = null) { return p\Mbstring::mb_detect_order($encodingList); } - function mb_parse_str($s, &$result = array()) { parse_str($s, $result); } - function mb_strlen($s, $enc = null) { return p\Mbstring::mb_strlen($s, $enc); } - function mb_strpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strpos($s, $needle, $offset, $enc); } - function mb_strtolower($s, $enc = null) { return p\Mbstring::mb_strtolower($s, $enc); } - function mb_strtoupper($s, $enc = null) { return p\Mbstring::mb_strtoupper($s, $enc); } - function mb_substitute_character($char = null) { return p\Mbstring::mb_substitute_character($char); } - function mb_substr($s, $start, $length = 2147483647, $enc = null) { return p\Mbstring::mb_substr($s, $start, $length, $enc); } - function mb_stripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_stripos($s, $needle, $offset, $enc); } - function mb_stristr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_stristr($s, $needle, $part, $enc); } - function mb_strrchr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrchr($s, $needle, $part, $enc); } - function mb_strrichr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrichr($s, $needle, $part, $enc); } - function mb_strripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strripos($s, $needle, $offset, $enc); } - function mb_strrpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strrpos($s, $needle, $offset, $enc); } - function mb_strstr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strstr($s, $needle, $part, $enc); } - function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } - function mb_http_output($enc = null) { return p\Mbstring::mb_http_output($enc); } - function mb_strwidth($s, $enc = null) { return p\Mbstring::mb_strwidth($s, $enc); } - function mb_substr_count($haystack, $needle, $enc = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $enc); } - function mb_output_handler($contents, $status) { return p\Mbstring::mb_output_handler($contents, $status); } - function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); } - function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { return p\Mbstring::mb_convert_variables($toEncoding, $fromEncoding, $a, $b, $c, $d, $e, $f); } -} -if (!function_exists('mb_chr')) { - function mb_ord($s, $enc = null) { return p\Mbstring::mb_ord($s, $enc); } - function mb_chr($code, $enc = null) { return p\Mbstring::mb_chr($code, $enc); } - function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); } -} diff --git a/vendor/symfony/polyfill-mbstring/composer.json b/vendor/symfony/polyfill-mbstring/composer.json deleted file mode 100644 index 50ea12f..0000000 --- a/vendor/symfony/polyfill-mbstring/composer.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "symfony/polyfill-mbstring", - "type": "library", - "description": "Symfony polyfill for the Mbstring extension", - "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=5.3.3" - }, - "autoload": { - "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, - "files": [ "bootstrap.php" ] - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "1.9-dev" - } - } -} diff --git a/vendor/true/punycode/CHANGELOG.md b/vendor/true/punycode/CHANGELOG.md deleted file mode 100644 index 39b44af..0000000 --- a/vendor/true/punycode/CHANGELOG.md +++ /dev/null @@ -1,45 +0,0 @@ -# Changelog - -## 2.1.0 - 2016-08-09 - -- [Enhancement] Increase rfc compliance (#20) - - Thanks to [@skroczek](https://github.com/skroczek) for the full patch. - -## 2.0.3 - 2016-05-23 - -- [Fix] Exclude development stuff from repository autogenerated ZIP archives (#18) - - Thanks to [@mlocati](https://github.com/mlocati) for the full patch. - -## 2.0.2 - 2016-01-07 - -- [Fix] Encode and decode domains regardless of their casing (#16) - - Thanks to [@abcdmitry](https://github.com/abcdmitry) for the full patch. - - -## 2.0.1 - 2015-09-01 - -- [Fix] Removed `version` property from `composer.json` file - - Thanks to [@GrahamCampbell](https://github.com/GrahamCampbell) for the patch. - - -## 2.0.0 - 2015-06-24 - -- [Enhancement] PHP 7 support -- [Fix] Renamed `True` namespace to `TrueBV` as it is a reserved word in PHP 7 - - -## 1.1.0 - 2015-03-12 - -- [Enhancement] Character encoding is now passed to the constructor, defaulting to UTF-8, as opposite to relying on `mb_internal_encoding` function call (#9). - - -## 1.0.1 - 2014-08-26 - -- [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) compliant and automation on Travis-CI - - Thanks to [@nyamsprod](https://github.com/nyamsprod) for initial patch. -- [Fix] Domain containing `x`, `n` or `-` would result in failures while decoding (#6). - - -## 1.0.0 - 2014-03-10 - -- Initial release diff --git a/vendor/true/punycode/LICENSE b/vendor/true/punycode/LICENSE deleted file mode 100644 index 963c72a..0000000 --- a/vendor/true/punycode/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2014 TrueServer B.V. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/vendor/true/punycode/README.md b/vendor/true/punycode/README.md deleted file mode 100644 index c290f97..0000000 --- a/vendor/true/punycode/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Punycode - -[![Build Status](https://secure.travis-ci.org/true/php-punycode.png?branch=master)](http://travis-ci.org/true/php-punycode) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/true/php-punycode/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/true/php-punycode/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/true/php-punycode/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/true/php-punycode/?branch=master) -[![Latest Stable Version](https://poser.pugx.org/true/punycode/version.png)](https://packagist.org/packages/true/punycode) - -A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA). - - -## Install - -``` -composer require true/punycode:~2.0 -``` - - -## Usage - -```php -encode('renangonçalves.com')); -// outputs: xn--renangonalves-pgb.com - -var_dump($Punycode->decode('xn--renangonalves-pgb.com')); -// outputs: renangonçalves.com -``` - - -## FAQ - -### 1. What is this library for? - -This library converts a Unicode encoded domain name to a IDNA ASCII form and vice-versa. - - -### 2. Why should I use this instead of [PHP's IDN Functions](http://php.net/manual/en/ref.intl.idn.php)? - -If you can compile the needed dependencies (intl, libidn) there is not much difference. -But if you want to write portable code between hosts (including Windows and Mac OS), or can't install PECL extensions, this is the right library for you. diff --git a/vendor/true/punycode/composer.json b/vendor/true/punycode/composer.json deleted file mode 100644 index 5b2815a..0000000 --- a/vendor/true/punycode/composer.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "true/punycode", - "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", - "keywords": ["IDNA", "punycode"], - "homepage": "https://github.com/true/php-punycode", - "license": "MIT", - "authors": [ - { - "name": "Renan Gonçalves", - "email": "renan.saddam@gmail.com" - } - ], - "autoload": { - "psr-4": { - "TrueBV\\": "src/" - } - }, - "require": { - "symfony/polyfill-mbstring": "^1.3", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.7", - "squizlabs/php_codesniffer": "~2.0" - } -} diff --git a/vendor/true/punycode/src/Exception/DomainOutOfBoundsException.php b/vendor/true/punycode/src/Exception/DomainOutOfBoundsException.php deleted file mode 100644 index f50fdc9..0000000 --- a/vendor/true/punycode/src/Exception/DomainOutOfBoundsException.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ -class DomainOutOfBoundsException extends OutOfBoundsException -{ - -} diff --git a/vendor/true/punycode/src/Exception/LabelOutOfBoundsException.php b/vendor/true/punycode/src/Exception/LabelOutOfBoundsException.php deleted file mode 100644 index e23b141..0000000 --- a/vendor/true/punycode/src/Exception/LabelOutOfBoundsException.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ -class LabelOutOfBoundsException extends OutOfBoundsException -{ - -} diff --git a/vendor/true/punycode/src/Exception/OutOfBoundsException.php b/vendor/true/punycode/src/Exception/OutOfBoundsException.php deleted file mode 100644 index 73b7229..0000000 --- a/vendor/true/punycode/src/Exception/OutOfBoundsException.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ -class OutOfBoundsException extends \RuntimeException -{ - -} diff --git a/vendor/true/punycode/src/Punycode.php b/vendor/true/punycode/src/Punycode.php deleted file mode 100644 index fbc54dd..0000000 --- a/vendor/true/punycode/src/Punycode.php +++ /dev/null @@ -1,360 +0,0 @@ - 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4, 'f' => 5, - 'g' => 6, 'h' => 7, 'i' => 8, 'j' => 9, 'k' => 10, 'l' => 11, - 'm' => 12, 'n' => 13, 'o' => 14, 'p' => 15, 'q' => 16, 'r' => 17, - 's' => 18, 't' => 19, 'u' => 20, 'v' => 21, 'w' => 22, 'x' => 23, - 'y' => 24, 'z' => 25, '0' => 26, '1' => 27, '2' => 28, '3' => 29, - '4' => 30, '5' => 31, '6' => 32, '7' => 33, '8' => 34, '9' => 35 - ); - - /** - * Character encoding - * - * @param string - */ - protected $encoding; - - /** - * Constructor - * - * @param string $encoding Character encoding - */ - public function __construct($encoding = 'UTF-8') - { - $this->encoding = $encoding; - } - - /** - * Encode a domain to its Punycode version - * - * @param string $input Domain name in Unicode to be encoded - * @return string Punycode representation in ASCII - */ - public function encode($input) - { - $input = mb_strtolower($input, $this->encoding); - $parts = explode('.', $input); - foreach ($parts as &$part) { - $length = strlen($part); - if ($length < 1) { - throw new LabelOutOfBoundsException(sprintf('The length of any one label is limited to between 1 and 63 octets, but %s given.', $length)); - } - $part = $this->encodePart($part); - } - $output = implode('.', $parts); - $length = strlen($output); - if ($length > 255) { - throw new DomainOutOfBoundsException(sprintf('A full domain name is limited to 255 octets (including the separators), %s given.', $length)); - } - - return $output; - } - - /** - * Encode a part of a domain name, such as tld, to its Punycode version - * - * @param string $input Part of a domain name - * @return string Punycode representation of a domain part - */ - protected function encodePart($input) - { - $codePoints = $this->listCodePoints($input); - - $n = static::INITIAL_N; - $bias = static::INITIAL_BIAS; - $delta = 0; - $h = $b = count($codePoints['basic']); - - $output = ''; - foreach ($codePoints['basic'] as $code) { - $output .= $this->codePointToChar($code); - } - if ($input === $output) { - return $output; - } - if ($b > 0) { - $output .= static::DELIMITER; - } - - $codePoints['nonBasic'] = array_unique($codePoints['nonBasic']); - sort($codePoints['nonBasic']); - - $i = 0; - $length = mb_strlen($input, $this->encoding); - while ($h < $length) { - $m = $codePoints['nonBasic'][$i++]; - $delta = $delta + ($m - $n) * ($h + 1); - $n = $m; - - foreach ($codePoints['all'] as $c) { - if ($c < $n || $c < static::INITIAL_N) { - $delta++; - } - if ($c === $n) { - $q = $delta; - for ($k = static::BASE;; $k += static::BASE) { - $t = $this->calculateThreshold($k, $bias); - if ($q < $t) { - break; - } - - $code = $t + (($q - $t) % (static::BASE - $t)); - $output .= static::$encodeTable[$code]; - - $q = ($q - $t) / (static::BASE - $t); - } - - $output .= static::$encodeTable[$q]; - $bias = $this->adapt($delta, $h + 1, ($h === $b)); - $delta = 0; - $h++; - } - } - - $delta++; - $n++; - } - $out = static::PREFIX . $output; - $length = strlen($out); - if ($length > 63 || $length < 1) { - throw new LabelOutOfBoundsException(sprintf('The length of any one label is limited to between 1 and 63 octets, but %s given.', $length)); - } - - return $out; - } - - /** - * Decode a Punycode domain name to its Unicode counterpart - * - * @param string $input Domain name in Punycode - * @return string Unicode domain name - */ - public function decode($input) - { - $input = strtolower($input); - $parts = explode('.', $input); - foreach ($parts as &$part) { - $length = strlen($part); - if ($length > 63 || $length < 1) { - throw new LabelOutOfBoundsException(sprintf('The length of any one label is limited to between 1 and 63 octets, but %s given.', $length)); - } - if (strpos($part, static::PREFIX) !== 0) { - continue; - } - - $part = substr($part, strlen(static::PREFIX)); - $part = $this->decodePart($part); - } - $output = implode('.', $parts); - $length = strlen($output); - if ($length > 255) { - throw new DomainOutOfBoundsException(sprintf('A full domain name is limited to 255 octets (including the separators), %s given.', $length)); - } - - return $output; - } - - /** - * Decode a part of domain name, such as tld - * - * @param string $input Part of a domain name - * @return string Unicode domain part - */ - protected function decodePart($input) - { - $n = static::INITIAL_N; - $i = 0; - $bias = static::INITIAL_BIAS; - $output = ''; - - $pos = strrpos($input, static::DELIMITER); - if ($pos !== false) { - $output = substr($input, 0, $pos++); - } else { - $pos = 0; - } - - $outputLength = strlen($output); - $inputLength = strlen($input); - while ($pos < $inputLength) { - $oldi = $i; - $w = 1; - - for ($k = static::BASE;; $k += static::BASE) { - $digit = static::$decodeTable[$input[$pos++]]; - $i = $i + ($digit * $w); - $t = $this->calculateThreshold($k, $bias); - - if ($digit < $t) { - break; - } - - $w = $w * (static::BASE - $t); - } - - $bias = $this->adapt($i - $oldi, ++$outputLength, ($oldi === 0)); - $n = $n + (int) ($i / $outputLength); - $i = $i % ($outputLength); - $output = mb_substr($output, 0, $i, $this->encoding) . $this->codePointToChar($n) . mb_substr($output, $i, $outputLength - 1, $this->encoding); - - $i++; - } - - return $output; - } - - /** - * Calculate the bias threshold to fall between TMIN and TMAX - * - * @param integer $k - * @param integer $bias - * @return integer - */ - protected function calculateThreshold($k, $bias) - { - if ($k <= $bias + static::TMIN) { - return static::TMIN; - } elseif ($k >= $bias + static::TMAX) { - return static::TMAX; - } - return $k - $bias; - } - - /** - * Bias adaptation - * - * @param integer $delta - * @param integer $numPoints - * @param boolean $firstTime - * @return integer - */ - protected function adapt($delta, $numPoints, $firstTime) - { - $delta = (int) ( - ($firstTime) - ? $delta / static::DAMP - : $delta / 2 - ); - $delta += (int) ($delta / $numPoints); - - $k = 0; - while ($delta > ((static::BASE - static::TMIN) * static::TMAX) / 2) { - $delta = (int) ($delta / (static::BASE - static::TMIN)); - $k = $k + static::BASE; - } - $k = $k + (int) (((static::BASE - static::TMIN + 1) * $delta) / ($delta + static::SKEW)); - - return $k; - } - - /** - * List code points for a given input - * - * @param string $input - * @return array Multi-dimension array with basic, non-basic and aggregated code points - */ - protected function listCodePoints($input) - { - $codePoints = array( - 'all' => array(), - 'basic' => array(), - 'nonBasic' => array(), - ); - - $length = mb_strlen($input, $this->encoding); - for ($i = 0; $i < $length; $i++) { - $char = mb_substr($input, $i, 1, $this->encoding); - $code = $this->charToCodePoint($char); - if ($code < 128) { - $codePoints['all'][] = $codePoints['basic'][] = $code; - } else { - $codePoints['all'][] = $codePoints['nonBasic'][] = $code; - } - } - - return $codePoints; - } - - /** - * Convert a single or multi-byte character to its code point - * - * @param string $char - * @return integer - */ - protected function charToCodePoint($char) - { - $code = ord($char[0]); - if ($code < 128) { - return $code; - } elseif ($code < 224) { - return (($code - 192) * 64) + (ord($char[1]) - 128); - } elseif ($code < 240) { - return (($code - 224) * 4096) + ((ord($char[1]) - 128) * 64) + (ord($char[2]) - 128); - } else { - return (($code - 240) * 262144) + ((ord($char[1]) - 128) * 4096) + ((ord($char[2]) - 128) * 64) + (ord($char[3]) - 128); - } - } - - /** - * Convert a code point to its single or multi-byte character - * - * @param integer $code - * @return string - */ - protected function codePointToChar($code) - { - if ($code <= 0x7F) { - return chr($code); - } elseif ($code <= 0x7FF) { - return chr(($code >> 6) + 192) . chr(($code & 63) + 128); - } elseif ($code <= 0xFFFF) { - return chr(($code >> 12) + 224) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128); - } else { - return chr(($code >> 18) + 240) . chr((($code >> 12) & 63) + 128) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128); - } - } -} diff --git a/webpack.config.js b/webpack.config.js index 8bbe0a8..fdf9ad2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const RemoveEmptyScripts = require('webpack-remove-empty-scripts'); const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin'); const autoprefixer = require('autoprefixer'); -const {getIfUtils, removeEmpty} = require('webpack-config-utils'); +const { getIfUtils, removeEmpty } = require('webpack-config-utils'); const { ifProduction } = getIfUtils(process.env.NODE_ENV); module.exports = { @@ -16,11 +16,11 @@ module.exports = { * Add your entry files here */ entry: { - 'css/broken-link-detector': './source/sass/broken-link-detector.scss', - 'js/broken-link-detector': './source/js/broken-link-detector.js', - 'js/mce-broken-link-detector': './source/mce/mce-broken-link-detector.js' + 'css/broken-link-detector': './source/sass/broken-link-detector.scss', + 'js/context-detector': './source/js/context-detector.ts', + 'js/editor-highlight': './source/js/editor-highlight.js' }, - + /** * Output settings */ @@ -29,13 +29,22 @@ module.exports = { path: path.resolve(__dirname, 'dist'), publicPath: '', }, + /** * Define external dependencies here */ - externals: { - }, + externals: {}, + module: { rules: [ + /** + * TypeScript and JavaScript + */ + { + test: /\.tsx?$/, // TypeScript and TSX file handling + use: 'ts-loader', + exclude: /node_modules/, + }, /** * Styles */ @@ -53,51 +62,56 @@ module.exports = { loader: 'postcss-loader', options: { postcssOptions: { - plugins: [ autoprefixer ], - } - } + plugins: [autoprefixer], + }, + }, }, { loader: 'sass-loader', - options: {} + options: {}, }, - 'import-glob-loader' + 'import-glob-loader', ], }, ], }, + + /** + * Add TypeScript resolution + */ + resolve: { + extensions: ['.tsx', '.ts', '.js'], // Resolve .ts and .tsx in addition to .js + }, + plugins: removeEmpty([ /** * Fix CSS entry chunks generating js file */ - new RemoveEmptyScripts(), + new RemoveEmptyScripts(), /** * Clean dist folder */ new CleanWebpackPlugin(), + /** * Output CSS files */ new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css' + filename: '[name].[contenthash].css', }), /** * Output manifest.json for cache busting */ new WebpackManifestPlugin({ - // Filter manifest items filter: function (file) { - // Don't include source maps if (file.path.match(/\.(map)$/)) { return false; } return true; }, - // Custom mapping of manifest item goes here map: function (file) { - // Fix incorrect key for fonts if ( file.isAsset && file.isModuleAsset && @@ -106,12 +120,11 @@ module.exports = { const pathParts = file.path.split('.'); const nameParts = file.name.split('.'); - // Compare extensions if (pathParts[pathParts.length - 1] !== nameParts[nameParts.length - 1]) { file.name = pathParts[0].concat('.', pathParts[pathParts.length - 1]); } } - + return file; }, }), @@ -119,23 +132,25 @@ module.exports = { /** * Enable build OS notifications (when using watch command) */ - new WebpackNotifierPlugin({alwaysNotify: true, skipFirstNotification: true}), + new WebpackNotifierPlugin({ alwaysNotify: true, skipFirstNotification: true }), /** * Minimize CSS assets */ - ifProduction(new CssMinimizerWebpackPlugin({ - minimizerOptions: { - preset: [ - "default", - { - discardComments: { removeAll: true }, - }, - ], - }, - })) - + ifProduction( + new CssMinimizerWebpackPlugin({ + minimizerOptions: { + preset: [ + 'default', + { + discardComments: { removeAll: true }, + }, + ], + }, + }) + ), ]).filter(Boolean), + devtool: 'source-map', - stats: { children: false } + stats: { children: false }, }; \ No newline at end of file