Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests/
phpunit.xml
70 changes: 58 additions & 12 deletions Plugin.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<?php namespace Bombozama\LinkCheck;
<?php

namespace Bombozama\LinkCheck;

use Bombozama\LinkCheck\Models\BrokenLink;
use System\Classes\PluginBase;
use Bombozama\LinkCheck\Models\Settings;
use Backend;
use Flash;
use Lang;
use System\Classes\PluginBase;
use Bombozama\LinkCheck\Models\Context;
use Bombozama\LinkCheck\Models\Settings;
use Bombozama\LinkCheck\ReportWidgets\BrokenLinks;

/**
* LinkCheck Plugin Information File
Expand All @@ -23,10 +27,24 @@ public function pluginDetails()
'description' => 'bombozama.linkcheck::lang.details.description',
'author' => 'Gonzalo Henríquez',
'icon' => 'icon-chain-broken',
'homepage' => 'https://github.com/bombozama/linkcheck'
'homepage' => 'https://github.com/bombozama/linkcheck',
];
}

public function register()
{
Settings::extend(function ($model) {
$model->bindEvent('model.beforeSave', function () use ($model) {
if (!post('Settings[plugins]')) {
// If no plugin was selected, remove the selected model fields
// by setting modelators to an empty string.
$model->modelators = '';
Flash::info(Lang::get('bombozama.linkcheck::lang.strings.plugins_empty'));
}
});
});
}

public function registerPermissions()
{
return [
Expand All @@ -38,6 +56,10 @@ public function registerPermissions()
'tab' => 'bombozama.linkcheck::lang.plugin.tab',
'label' => 'bombozama.linkcheck::lang.plugin.view',
],
'bombozama.linkcheck.useragent' => [
'tab' => 'bombozama.linkcheck::lang.plugin.tab',
'label' => 'bombozama.linkcheck::lang.plugin.useragent',
],
];
}

Expand All @@ -51,16 +73,25 @@ public function registerSettings()
'icon' => 'icon-chain-broken',
'class' => 'Bombozama\LinkCheck\Models\Settings',
'order' => 410,
'permissions' => ['bombozama.linkcheck.manage']
'permissions' => ['bombozama.linkcheck.manage'],
],
'brokenlinks' => [
'label' => 'bombozama.linkcheck::lang.menu.brokenlinks.label',
'description' => 'bombozama.linkcheck::lang.menu.brokenlinks.description',
'category' => 'bombozama.linkcheck::lang.plugin.category',
'icon' => 'icon-list',
'url' => Backend::url('bombozama/linkcheck/brokenlinks'),
'url' => Backend::url('bombozama/linkcheck/context'),
'order' => 411,
'permissions' => ['bombozama.linkcheck.view']
'permissions' => ['bombozama.linkcheck.view'],
],
'userAgent' => [
'label' => 'bombozama.linkcheck::lang.menu.useragent.label',
'description' => 'bombozama.linkcheck::lang.menu.useragent.description',
'category' => 'bombozama.linkcheck::lang.plugin.category',
'icon' => 'icon-user-secret',
'url' => Backend::url('bombozama/linkcheck/useragent'),
'order' => 412,
'permissions' => ['bombozama.linkcheck.useragent'],
],
];
}
Expand All @@ -74,15 +105,30 @@ public function registerListColumnTypes()

public function httpStatus($value, $column, $record)
{
return '<span title="' . Lang::get('bombozama.linkcheck::lang.codes.' . $value ) . '">' . $value . '</span>';
return '<span title="' . Lang::get('bombozama.linkcheck::lang.codes.' . $value) . '">' . $value . '</span>';
}

public function registerReportWidgets()
{
return [
BrokenLinks::class => [
'label' => 'bombozama.linkcheck::lang.details.name',
'context' => 'dashboard',
'permissions' => [
'bombozama.linkcheck.view',
],
],
];
}

// Please do https://octobercms.com/docs/setup/installation#crontab-setup
public function registerSchedule($schedule)
{
$settings = Settings::instance();
$schedule->call(function(){
BrokenLink::processLinks();
})->cron($settings->time);
if ($settings->time) {
$schedule->call(function() {
Context::processLinks();
})->cron($settings->time);
}
}
}
9 changes: 9 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# LinkCheck Plugin for OctoberCMS
Schedules a task to check for broken links in database fields and/or CMS pages.

## Refactoring
* The plugin has been revised and optimised for October v3.x.
* DB: Splitting of URL/status and contexts (CMS pages, model fields) in which the URLs are located
* URL-based approach: URLs are stored in a set so that URLs are only checked once
* cURL only calls headers

## Features
* Checks for broken links within database fields and CMS text files
* User controls which fields to check
* User controls which response codes should be logged
* User controls task scheduling
* Management and use of user agents (new in v2.x)
* Dashboard report widget with overview (new in v2.x)
* Selection of plugins and directories for the check in the settings (new in v2.x)

## Usage
A link status report can be found in the backend Settings tab.
Expand Down
99 changes: 79 additions & 20 deletions classes/Helper.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
<?php namespace Bombozama\LinkCheck\Classes;
<?php

namespace Bombozama\LinkCheck\Classes;

class Helper
{
public static function scanForUrls($string)
public function scanForUrls(string $string): array
{
$urls = array();
$expression = '#\bhttps?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#';

preg_match_all($expression, $string, $matches);
return ($matches[0]);

foreach($matches[0] as $match) {
try {
$url = parse_url($match);

// Use URL without fragment
$parsed_url = $url['scheme'] . '://' . $url['host'];

if (isset($url['path'])) {
$parsed_url .= $url['path'];
}

if (isset($url['query'])) {
$parsed_url .= '?' . $url['query'];
}

$urls[] = $parsed_url;

} catch (\Exception $ex) {
// If URL is invalid / malformed, log an error
\Log::info('LinkCheck Error: Could not parse URL: ' . $match . ' due to the following error: ' . $ex);
}
}

return $urls;
}

public static function getFullClassNameFromFile($pathToFile)
Expand All @@ -15,47 +43,78 @@ public static function getFullClassNameFromFile($pathToFile)
$class = $namespace = $buffer = '';
$i = 0;
while (!$class) {
if (feof($fp))
if (feof($fp)) {
break;
}

$buffer.= fread($fp, 512);
$tokens = token_get_all($buffer);

if (strpos($buffer, '{') === false)
if (strpos($buffer, '{') === false) {
continue;
}

for (; $i < count($tokens); $i++) {
if($tokens[$i][0] === T_NAMESPACE)
for($j = $i+1; $j < count($tokens); $j++)
if($tokens[$j][0] === T_STRING)
if ($tokens[$i][0] === T_NAMESPACE) {
for ($j = $i+1; $j < count($tokens); $j++) {
if ($tokens[$j][0] === 265) { // PHP token id 265 == string
$namespace .= '\\' . $tokens[$j][1];
else if($tokens[$j] === '{' || $tokens[$j] === ';')
} elseif ($tokens[$j] === '{' || $tokens[$j] === ';') {
break;

if($tokens[$i][0] === T_CLASS)
for($j = $i+1; $j < count($tokens); $j++)
if($tokens[$j] === '{')
$class = $tokens[$i+2][1];
}
}
}
if ($tokens[$i][0] === T_CLASS) {
for ($j = $i+1; $j < count($tokens); $j++) {
if ($tokens[$j] === '{') {
if (is_array($tokens[$i+2]) && $tokens[$i+2][0] !== 392) { // PHP token id 392 == whitespace
$class = $tokens[$i+2][1];
}
}
}
}
}
}
return $namespace . '\\' . $class;
}

public static function getResponseCode($url)
public static function getResponseCode(string $url, mixed $userAgent): int
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch , CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 30000); # Setting timeout to 30 seconds
if ($userAgent) {
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
}
$data = curl_exec($ch);
$headers = curl_getinfo($ch);
curl_close($ch);

# In case of timeouts, let's throw out a 408 error "Request timeout"
if($headers['http_code'] == 0)
$headers['http_code'] = '408';

if ($headers['http_code'] == 0) {
$headers['http_code'] = 408;
}
return $headers['http_code'];
}

/**
* Flatten an arbitrarily nested array
*
* see https://www.lambda-out-loud.com/posts/flatten-arrays-php/
*
* @param array
* @return array
*/
public function flattenArray(array $array): array
{
$recursiveArrayIterator = new \RecursiveArrayIterator(
$array,
\RecursiveArrayIterator::CHILD_ARRAYS_ONLY
);
$iterator = new \RecursiveIteratorIterator($recursiveArrayIterator);

return iterator_to_array($iterator, false);
}

}
37 changes: 37 additions & 0 deletions controllers/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Bombozama\LinkCheck\Controllers;

use BackendMenu;
use Backend\Classes\Controller;
use System\Classes\SettingsManager;
use Flash;
use Lang;

/**
* Broken Links Back-end Controller
*/
class Context extends Controller
{
public $requiredPermissions = ['bombozama.linkcheck.view'];
public $implement = [
'Backend.Behaviors.ListController',
];
public $listConfig = 'config_list.yaml';

public function __construct()
{
parent::__construct();
BackendMenu::setContext('October.System', 'system', 'settings');
SettingsManager::setContext('Bombozama.LinkCheck', 'brokenlinks');
}

public function onRefreshLinkList()
{
Flash::warning(Lang::get('bombozama.linkcheck::lang.strings.working'));
$links = \Bombozama\Linkcheck\Models\Context::processLinks();
Flash::success(Lang::get('bombozama.linkcheck::lang.strings.total_links', ['number' => $links['reported'], 'total' => $links['checked']]));

return $this->listRefresh();
}
}
46 changes: 46 additions & 0 deletions controllers/UserAgent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Bombozama\Linkcheck\Controllers;

use BackendMenu;
use Backend\Classes\Controller;
use System\Classes\SettingsManager;

/**
* User Agent Backend Controller
*
* @link https://docs.octobercms.com/3.x/extend/system/controllers.html
*/
class UserAgent extends Controller
{
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class,
];

/**
* @var string formConfig file
*/
public $formConfig = 'config_form.yaml';

/**
* @var string listConfig file
*/
public $listConfig = 'config_list.yaml';

/**
* @var array required permissions
*/
public $requiredPermissions = ['bombozama.linkcheck.useragent'];

/**
* __construct the controller
*/
public function __construct()
{
parent::__construct();

BackendMenu::setContext('October.System', 'system', 'settings');
SettingsManager::setContext('Bombozama.LinkCheck', 'userAgent');
}
}
7 changes: 7 additions & 0 deletions controllers/context/_list_toolbar.htm
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php if (BackendAuth::userHasAccess('bombozama.linkcheck.manage')): ?>
<div data-control="toolbar">
<button type="button" data-request="onRefreshLinkList" class="btn btn-primary oc-icon-plus">
<?= e(trans('bombozama.linkcheck::lang.strings.refresh_list')) ?>
</button>
</div>
<?php endif ?>
Loading