Hi,
maybe I missed something important, but
to me it seems like the 404 pages listed on admin/config/search/redirect/404 are not language aware.

E.g. it could be that a path "about-our-campaign" is a valid page in English, but "fr/about-our-campaign" is a 404.
Then the list does only show that "about-our-campaign" is 404.

Is this observation correct?

CommentFileSizeAuthor
#193 1559310-193-remove_redirect_404_language_dependency.patch308 bytesstella
#184 404_language_aware-1559310-184.patch97.59 KBtduong
#184 interdiff-1559310-182-184.txt1.55 KBtduong
#182 r404_not_enabled_user_has_permission.png125.3 KBtduong
#182 r404_enabled.png128.91 KBtduong
#182 404_language_aware-1559310-182.patch96.06 KBtduong
#182 interdiff-1559310-179-182.txt2.93 KBtduong
#179 404_language_aware-1559310-179.patch94.03 KBtduong
#179 interdiff-1559310-176-179.txt4.04 KBtduong
#176 404_language_aware-1559310-176-interdiff.txt995 bytesBerdir
#176 404_language_aware-1559310-176.patch93.63 KBBerdir
#175 404_language_aware-1559310-175.patch93.63 KBtduong
#175 interdiff-1559310-173-175.txt758 bytestduong
#173 404_language_aware-1559310-173.patch93.66 KBtduong
#173 interdiff-1559310-172-173.txt6.57 KBtduong
#172 404_language_aware-1559310-172.patch91.51 KBtduong
#172 interdiff-1559310-169-172.txt5.64 KBtduong
#169 404_language_aware-1559310-169.patch90.46 KBtduong
#169 interdiff-1559310-168-169.txt2.5 KBtduong
#168 404_language_aware-1559310-168.patch88.04 KBtduong
#168 interdiff-1559310-163-168.txt602 bytestduong
#163 404_language_aware-1559310-163.patch88.04 KBtduong
#162 404_language_aware-1559310-161.patch82.38 KBtduong
#162 interdiff-1559310-157-161.txt4.45 KBtduong
#157 404_language_aware-1559310-157.patch81.97 KBtduong
#157 404_language_aware-1559310-157-utf8_test_only.patch1.82 KBtduong
#157 interdiff-1559310-154-157.txt1.52 KBtduong
#154 Screen Shot 2016-10-07 at 14.57.23.png49.41 KBtduong
#154 404_language_aware-1559310-154.patch81.61 KBtduong
#154 404_language_aware-1559310-154_test_only.patch11 KBtduong
#154 interdiff-1559310-150-154.txt7.39 KBtduong
#153 redirect-invalid.patch652 bytesBerdir
#152 diff_1559310_404lang_151.interdiff.txt4.03 KBmiro_dietiker
#150 interdiff-1559310-136-150.txt1.34 KBtoncic
#150 404_pages_should_be-1559310-150.patch84.49 KBtoncic
#146 interdiff-1559310-142-146.txt534 bytestoncic
#146 404_pages_should_be-1559310-146.patch171.82 KBtoncic
#145 interdiff-1559310-136-142.txt1.05 KBtoncic
#142 404_pages_should_be-1559310-142.patch171.82 KBtoncic
#136 interdiff.txt3.15 KBlarowlan
#136 lang-aware-1559310.136.patch84.46 KBlarowlan
#132 404_language_aware-1559310-132.patch82.55 KBtduong
#132 interdiff-1559310-129-132.txt1.96 KBtduong
#129 404_language_aware-1559310-129.patch82.54 KBtduong
#129 interdiff-1559310-124-129.txt10.11 KBtduong
#124 404_language_aware-1559310-124.patch77.24 KBtduong
#124 interdiff-1559310-123-124.txt798 bytestduong
#124 interdiff-1559310-120-124.txt2.43 KBtduong
#123 404_language_aware-1559310-123.patch77.19 KBtduong
#123 interdiff-1559310-121-123.txt1.8 KBtduong
#123 interdiff-1559310-120-123.txt2.46 KBtduong
#121 redirect settings.png226.18 KBtduong
#121 404_language_aware-1559310-121.patch77.23 KBtduong
#121 interdiff-1559310-120-121.txt2.9 KBtduong
#120 404_language_aware-1559310-120.patch77.39 KBtduong
#120 interdiff-1559310-118-120.txt1011 bytestduong
#118 404_language_aware-1559310-118.patch77.12 KBtduong
#118 interdiff-1559310-115-118.txt28.29 KBtduong
#115 404_language_aware-1559310-115.patch70.71 KBtduong
#115 interdiff-1559310-113-115.txt21.25 KBtduong
#113 404_language_aware-1559310-113.patch72.49 KBtduong
#113 interdiff-1559310-112-113.txt984 bytestduong
#112 404_language_aware-1559310-112.patch72.62 KBtduong
#112 interdiff-1559310-109-112.txt787 bytestduong
#109 404_language_aware-1559310-109.patch72.62 KBtduong
#109 interdiff-1559310-106-109.txt4.08 KBtduong
#106 404_language_aware-1559310-106.patch71.7 KBtduong
#106 interdiff-1559310-103-106.txt15.42 KBtduong
#103 Screen Shot 2016-06-21 at 19.20.44.png86.57 KBtduong
#103 404_language_aware-1559310-103.patch69.06 KBtduong
#103 interdiff-1559310-102-103.txt11.95 KBtduong
#102 404_language_aware-1559310-102.patch66.48 KBtduong
#102 interdiff-1559310-100-102.txt49.2 KBtduong
#100 404_language_aware-1559310-100.patch66.44 KBtduong
#100 interdiff-1559310-94-100.txt42.73 KBtduong
#99 redirect assigned.png88.35 KBtduong
#99 redirect.png95.64 KBtduong
#99 add redirect.png115 KBtduong
#99 view filters.png195.26 KBtduong
#99 Fix 404 pages view.png170.21 KBtduong
#99 Screen Shot 2016-06-17 at 17.48.07.png27.87 KBtduong
#99 Screen Shot 2016-06-17 at 17.47.37.png30.58 KBtduong
#94 404_language_aware-1559310-94.patch65.87 KBtduong
#94 interdiff-1559310-89-94.txt16.38 KBtduong
#89 404_language_aware-1559310-89.patch65.53 KBtduong
#89 interdiff-1559310-86-89.txt2.65 KBtduong
#86 404_language_aware-1559310-86.patch65.64 KBtduong
#86 interdiff-1559310-79-86.txt2.66 KBtduong
#79 404_language_aware-1559310-79.patch64.58 KBtduong
#79 interdiff-1559310-75-79.txt2.38 KBtduong
#75 404_language_aware-1559310-75.patch63.52 KBtduong
#75 interdiff-1559310-71-75.txt3.74 KBtduong
#71 404_language_aware-1559310-71.patch63.62 KBtduong
#71 interdiff-1559310-68-71.txt3 KBtduong
#68 404_language_aware-1559310-68.patch61.34 KBtduong
#68 interdiff-1559310-65-68.txt8.48 KBtduong
#65 404_language_aware-1559310-65.patch60.47 KBtduong
#65 interdiff-1559310-64-65.txt23.71 KBtduong
#64 404_language_aware-1559310-64.patch42.21 KBtduong
#64 interdiff-1559310-61-64.txt6.28 KBtduong
#61 Screen Shot 2016-06-09 at 18.27.16.png136.37 KBtduong
#61 404_language_aware-1559310-61.patch40.96 KBtduong
#61 interdiff-1559310-60-61.txt11.57 KBtduong
#60 Screen Shot 2016-06-09 at 12.35.57.png88.56 KBtduong
#60 404_language_aware-1559310-60.patch45.92 KBtduong
#60 interdiff-1559310-59-60.txt4.93 KBtduong
#59 404_language_aware-1559310-59.patch41.29 KBtduong
#59 interdiff-1559310-58-59.txt7.71 KBtduong
#58 404_language_aware-1559310-58.patch32.74 KBtduong
#58 interdiff-1559310-57-58.txt4.54 KBtduong
#57 404_language_aware-1559310-57.patch31.96 KBtduong
#57 interdiff-1559310-53-57.txt6.31 KBtduong
#53 404_language_aware-1559310-53.patch31.23 KBtduong
#53 interdiff-1559310-50-53.txt6.95 KBtduong
#50 404_language_aware-1559310-50.patch26.81 KBtduong
#50 interdiff-1559310-49-50.txt1.81 KBtduong
#49 404_language_aware-1559310-49.patch26.6 KBtduong
#49 interdiff-1559310-45-49.txt893 bytestduong
#46 404_language_aware-1559310-45.patch26.57 KBtduong
#46 interdiff-1559310-41-45.txt5.61 KBtduong
#41 404_language_aware-1559310-41.patch28.22 KBtduong
#41 interdiff-1559310-36-41.txt10.34 KBtduong
#36 404_language_aware-1559310-36.patch35.07 KBtduong
#36 interdiff-1559310-33-36.txt1.88 KBtduong
#33 404_language_aware-1559310-33.patch34.71 KBtduong
#33 interdiff-1559310-30-33.txt40.17 KBtduong
#30 fix 404 pages after.png113.28 KBtduong
#30 fix 404 pages before.png107.58 KBtduong
#30 settings after.png222.63 KBtduong
#30 settings before.png210.55 KBtduong
#30 404_language_aware-1559310-30.patch23.88 KBtduong
#30 interdiff-1559310-27-30.txt1.27 KBtduong
#27 404_language_aware-1559310-27.patch23.27 KBtduong
#27 interdiff-1559310-24-27.txt3.18 KBtduong
#24 404_language_aware-1559310-23.patch23.28 KBtduong
#24 interdiff-1559310-19-23.txt10.57 KBtduong
#19 404_language_aware-1559310-19.patch21.23 KBtduong
#19 interdiff-1559310-16-19.txt4.7 KBtduong
#16 404_language_aware-1559310-16.patch19.93 KBtduong
#9 redirect_error_155931_9.patch9.98 KBmiro_dietiker
#6 redirect_error_1559310.patch6.16 KBmiro_dietiker
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

Can confirm this, had the same situation today on a site that confused the customer and resulted in him adding redirects for existing pages.

Not sure if it can be fixed, however. Core would have to log the original path before the language prefix was removed, redirect.module only displays whatever information is in the page not found log.

miro_dietiker’s picture

I guess if data from core is missing, redirect should capture it on its own...

There's no specific 404 / page_not_found hook.

A 404 is created here finally via deliver_page:
http://api.drupal.org/api/drupal/includes%21common.inc/function/drupal_n...

The problem with fast404 is, we can NOT react on them so they're missing in the reports.

So let's create a new error table in redirect as submodule.

Then record error data for 404s via something like hook_page_build() if enabled (by default) including language.
Additionally, let's import apaches error.log in a cron. Thus we can drop complexity on 404s (no inserts) AND we can even import fast404's.
We need to check for logrotate cases (if growing, incremental. If smaller, restart. Or always check first line for change.)

Finally we can offer a batch import of error.log's if they where missing.

I'd be happy to hear from the maintainer for his opinion and if we should go ahead.
This all is also possible in a separate contrib.

Dave Reid’s picture

Hrm, I am wondering if redirect should implements it's own hook_watchdog() to record only 404 responses in it's own table rather than relying on dblog. It would make sense.

Additionally, let's import apaches error.log in a cron. Thus we can drop complexity on 404s (no inserts) AND we can even import fast404's.
We need to check for logrotate cases (if growing, incremental. If smaller, restart. Or always check first line for change.)

Finally we can offer a batch import of error.log's if they where missing.

This is out of scope for the module since it's implementation specific and could live separately in another module that feeds in the redirect 404 log table. And wouldn't raw server logs also have the same problem of missing the language context?

miro_dietiker’s picture

Raw server logs perfectly contain the language prefix (if it's a path prefix).
Drupal cuts this off in bootstrap with custom code.

hook_watchdog is a good idea. didn't find that...

We will end up with two db writes per 404. And if we not only append lines but e.g. try to select url & update counter, we will even end in a read-write-cycle, making most db's (especially with indexes) slow.

Then a variable to disable the 404 writes would be enough (so we can rely on the table and UI)

Dave Reid’s picture

There is no counter. It's only one write per 404 as far as redirect would be concerned. Not any different from having dblog do a write per 404 or 403. And yeah, making it controlled by a variable (enabled by default) so that other methods could populate the table would give the most flexibility.

miro_dietiker’s picture

Status: Active » Needs review
FileSize
6.16 KB

Here's a first step.
Introducing the schema table redirect_error, capturing all drupal_not_found as records.
Not really tested yet. Let's check testbots statuses.

Missing is

  • Data migration from watchdog
  • Test coverage
  • Capture language data correct
  • Pass language to redirect on creation

Also check the code @todo.

miro_dietiker’s picture

Issue tags: +Needs tests

Hmm, testing postponed... It seems redirect has an exception in the tests now "Infinite redirect loop prevented."

Berdir’s picture

+++ b/redirect.installundefined
@@ -356,3 +357,68 @@ function _redirect_migrate_path_redirect_variables() {
+  db_create_table('redirect_error', _redirect_schema_redirect_error());

The table definition needs to be copied, a helper function doesn't work. The schema could change and then you need to change hook_schema() but not the initial update function.

+++ b/redirect.installundefined
@@ -356,3 +357,68 @@ function _redirect_migrate_path_redirect_variables() {
+  // @todo migrate data from watchdog?
+  $return = '';
+
+  return $return;

No need to return anything if there is no relevant information.

Not sure if we need a data migration? This will get built up again and watchdog is only temporary data anyway.

+++ b/redirect.installundefined
@@ -356,3 +357,68 @@ function _redirect_migrate_path_redirect_variables() {
+      'uid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'The {users}.uid of the user who created the redirect.',

description seems wrong?

+++ b/redirect.installundefined
@@ -356,3 +357,68 @@ function _redirect_migrate_path_redirect_variables() {
+      'status_code' => array(
+        'type' => 'int',
+        'size' => 'small',
+        'not null' => TRUE,
+        'description' => 'The HTTP status code to use for the redirect.',

What's this for?

+++ b/redirect.installundefined
@@ -356,3 +357,68 @@ function _redirect_migrate_path_redirect_variables() {
+      'timestamp' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'The timestamp of when the redirect was last accessed.'

Description also wrong I think.

+++ b/redirect.moduleundefined
@@ -1565,3 +1565,24 @@ function redirect_redirect_operations() {
+  if ($log_entry['type']=='page not found') {

Missing spaces around ==

+++ b/redirect.moduleundefined
@@ -1565,3 +1565,24 @@ function redirect_redirect_operations() {
+      // @todo fix for cases where language prefix is missing.
+      'language' => $language->language,

Not sure I understand the @todo, there is always a $language object and ->language is also always set. This should be fine I think.

miro_dietiker’s picture

Schema: Fully agree with the needed schema definition duplication. I was just looking a bit around and did a copy paste from ???

Most minor thingies. Most fixed already.
Migration dropped.

I added a status column to cover e.g. other 40x errors if needed...
This corresponds to the status code, a redirect sends (302, ...) and how redirect works.
In addition to 404 i thought about covering the case 403 (access denied). But note that a redirect doesn't really make sense there...
So yes, removed

'language' => $language->language,
The problem here is that $language has always a value.
If for example a callback comes WITHOUT a language prefix, we want to identify this case and add a language neutral redirect.
Since drupal has a default language, the default context is identified and only a redirect for that default language is added.
At least in case of switching the default language, all the added redirects don't work again.

I guess the right direction is to be found here:
http://api.drupal.org/api/drupal/includes%21language.inc/group/language_...
module_load_include('language', 'inc', 'language.negotiation');
$langcode = locale_language_from_url($languages);
http://api.drupal.org/api/drupal/includes%21locale.inc/function/locale_l...

Additionally added detection of multilingual case.

Missing is still

  • Test coverage
  • Pass language to redirect on creation
knalstaaf’s picture

Issue summary: View changes

Both patches give me this raw code displayed on every page:

return $operations; } /** * Implements hook_watchdog. * * Write a redirect_error per 404. */ function redirect_watchdog(array $log_entry) { if ($log_entry['type'] == 'page not found') { if (!variable_get('redirect_error_log', TRUE)) { return; } global $user; global $language; $langcode = $language->language; if (module_exists('locale')) { require_once DRUPAL_ROOT . '/includes/language.inc'; module_load_include('locale.inc', 'locale'); // Fetch language from URL. $languages = language_list(); // We need a workaround for language negotiation as the language is already cutoff from $_GET['q']. $saveq = $_GET['q']; $_GET['q'] = request_path(); $url_langcode = locale_language_from_url($languages); $langcode = $url_langcode===FALSE?'':$url_langcode; $_GET['q'] = $saveq; } // Write record. $record = array( 'source' => $log_entry['message'], 'uid' => $user->uid, // @todo fix for cases where language prefix is missing. 'language' => $langcode, 'status_code' => '404', 'timestamp' => $log_entry['timestamp'], ); drupal_write_record('redirect_error', $record); } 

And these kind of errors:

Warning: Cannot modify header information - headers already sent by (output started at /home/altex/public_html/sites/all/modules/redirect/redirect.module:40) in drupal_send_headers() (line 1228 of /home/altex/public_html/includes/bootstrap.inc).
Warning: Cannot modify header information - headers already sent by (output started at /home/altex/public_html/sites/all/modules/redirect/redirect.module:40) in drupal_goto() (regel 698 van /home/altex/public_html/includes/common.inc).

I'm using Redirect 7.x-1.0-rc1+8-dev (2014-Jul-23).

Dave Reid’s picture

Status: Needs review » Needs work

The last submitted patch, 9: redirect_error_155931_9.patch, failed testing.

miro_dietiker’s picture

Issue tags: +8.x release target

Added this to our 8.x port board.

Dave Reid’s picture

Version: 7.x-1.x-dev » 7.x-2.x-dev
Issue tags: +7.x-2.0 release blocker
Parent issue: #2514248: Plan for Redirect v7.x-1.0 release »
tduong’s picture

Version: 7.x-2.x-dev » 8.x-1.x-dev
Status: Needs work » Needs review
FileSize
19.93 KB

Yesterday redirect has been pushed to d.o, so changed version to 8.x-1.x-dev and the PR #98 will be moved here.

Uploaded current patch: first point of the PR is done + starting code for the RedirectNotFoundStorage service.

Status: Needs review » Needs work

The last submitted patch, 16: 404_language_aware-1559310-16.patch, failed testing.

The last submitted patch, 16: 404_language_aware-1559310-16.patch, failed testing.

tduong’s picture

Fixed/completed RedirectNotFoundStorage service.
There are still a \Drupal::database() call in RedirectFix404Form (line 80) and redirect.install (line 230). Should we use a service also for them ?

Status: Needs review » Needs work

The last submitted patch, 19: 404_language_aware-1559310-19.patch, failed testing.

The last submitted patch, 19: 404_language_aware-1559310-19.patch, failed testing.

Dave Reid’s picture

If we're using our own table (which is good), I think we should write Views integration instead and use that to display this page, instead of hard-coded page output.

Debatable also if this should be moved to its own separate sub-module (a redirect_404 sub-module, and also splitting out the 'expire + counting + last access' into a redirect_stats sub-module).

Berdir’s picture

  1. +++ b/src/Entity/Redirect.php
    @@ -31,7 +31,8 @@ use Drupal\link\LinkItemInterface;
      *     "views_data" = "Drupal\views\EntityViewsData",
    - *     "storage_schema" = "\Drupal\redirect\RedirectStorageSchema"
    + *     "storage_schema" = "\Drupal\redirect\RedirectStorageSchema",
    + *     "storage_not_found" = "\Drupal\redirect\RedirectNotFoundStorage",
      *   },
    

    this is not an entity handler, just the service is enough. so you can remove this again.

  2. +++ b/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,47 @@
    +      if (!\Drupal::config('redirect.settings')->get('log_404')) {
    +        return;
    

    and config factory should be injected too.

  3. +++ b/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,47 @@
    +
    +      /** @var \Drupal\redirect\RedirectNotFoundStorage $redirect_storage */
    +      $redirect_storage = \Drupal::service('redirect.not_found_storage');
    +      $redirect_storage->mergeRedirect404Records();
    

    the storage shouldn't have to know about where the path is coming from. Make a method with specific arguments, something like logRequest($path, $langcode)

    You should also inject the storage into this service.

  4. +++ b/src/Form/RedirectFix404Form.php
    @@ -55,72 +47,69 @@ class RedirectFix404Form extends FormBase {
    +    $query = $redirect_storage->getRedirect404Table($header);
    

    this should not return a query but just plain result objects.

    The idea of the service is that it must be able to implement it with a different backend that's not about sql at all.

    So you need to pass the $search string to it as well.

    $header is a bit tricky, the paging as well.

    About the method name, I'd do something like listRequests() or so.

  5. +++ b/src/Form/RedirectFix404Form.php
    @@ -55,72 +47,69 @@ class RedirectFix404Form extends FormBase {
    +      $wildcard = '%' . trim(preg_replace('!\*+!', '%', \Drupal::database()->escapeLike($search)), '%') . '%';
    

    then this problem will resolve itself as it will be inside the service as well.

Crossposted with Dave, I'm OK with making this a separate redirect_404 module, instead of a setting.

Note that I suggested to make storage a service so that a faster backend like redis could be used. That conflicts with using views for it. Not sure.

Also note that the count/expire stuff has been removed already from redirect 8.x-1.x as it simply doesn't work with how we do caching now. So it would be re-implementing in a new module, not splitting :) And if it should work with page cache, it would have to be a middleware that runs before page cache.

tduong’s picture

Fixed points in the comment above, just "$header is a bit tricky, the paging as well" left.
Fixed RedirectUILanguageTest test + some refactoring.

Moving stuff into the sub-module will be done later as it would be hard to review.

Status: Needs review » Needs work

The last submitted patch, 24: 404_language_aware-1559310-23.patch, failed testing.

The last submitted patch, 24: 404_language_aware-1559310-23.patch, failed testing.

tduong’s picture

The test fails because it is still using the old redirect_error table fields. Updates in RedirectUILanguageTest and RedirectFix404Form.

Status: Needs review » Needs work

The last submitted patch, 27: 404_language_aware-1559310-27.patch, failed testing.

The last submitted patch, 27: 404_language_aware-1559310-27.patch, failed testing.

tduong’s picture

Fixed a line in RedirectUITest, but there is still some bugs that I don't know how to fix. It seems like when you want to add a redirect, it does not have a placeholder/pre-set a value in redirect_source[0][path] textfield, neither is language[0][value] (default to - Not specified - (und)). So these are 2 failing assertions, plus since Path field is required and has no pre-filled path value, after saving it "redirects back" to the same page, thus another assert fail.
Both of them appear in RedirectUITest and RedirectUILanguageTest.

Furthermore in the language test, from count(db_select('redirect_404')->fields('redirect_404')->condition('path', 'testing')->execute()->fetchAll()) it returns 0, even though it should be 1 (I've tried also with condition('count', 'testing')).

Then I've tested it manually and I'm not sure if it was meant to be so, but in the "Fix 404 pages" it has dropped the langcode from the path that was part of it if you see how it looks like without the patch applied (didn't check the past commits yet).

I'm not sure how to fix these bugs left. Will wait for some feedbacks and reviews/approval to move the redirect_404 stuff into a submodule.

Status: Needs review » Needs work

The last submitted patch, 30: 404_language_aware-1559310-30.patch, failed testing.

The last submitted patch, 30: 404_language_aware-1559310-30.patch, failed testing.

tduong’s picture

Moved (almost) every 404 related files/codes into redirect_404 submodule, fixed where was needed.

Status: Needs review » Needs work

The last submitted patch, 33: 404_language_aware-1559310-33.patch, failed testing.

The last submitted patch, 33: 404_language_aware-1559310-33.patch, failed testing.

tduong’s picture

Didn't run the tests before uploading the patch, but noticed that Fix404RedirectUILanguageTest takes more than 6 mins... what is wrong here ?

Dropped invalid taxonomy permission in both tests.

Status: Needs review » Needs work

The last submitted patch, 36: 404_language_aware-1559310-36.patch, failed testing.

The last submitted patch, 36: 404_language_aware-1559310-36.patch, failed testing.

tduong’s picture

For the fix 404 redirect form *not actually* pre-filling its fields, I think that it may be a bug form RedirectFix404Form::buildForm() which leads to RedirectForm::prepareEntity() with no value for redirect_source[0][path] and language[0][value] (set as und per default), but not sure what is exactly the bug...

Berdir’s picture

  1. +++ b/modules/redirect_404/config/install/redirect_404.settings.yml
    @@ -0,0 +1 @@
    +log_404: true
    
    +++ b/modules/redirect_404/config/schema/redirect_404.schema.yml
    @@ -0,0 +1,9 @@
    +redirect_404.settings:
    +  type: config_entity
    +  label: 'Redirect 404 settings'
    +  mapping:
    +    log_404:
    +      type: boolean
    +      label: 'Log 404 errors'
    

    I think we no longer need the setting then. If you don't want this feature, don't enable the module.

  2. +++ b/modules/redirect_404/redirect_404.info.yml
    @@ -0,0 +1,10 @@
    +dependencies:
    + - link
    + - views
    + - redirect
    

    we only need to specify redirect as dependency I think.

  3. +++ b/modules/redirect_404/redirect_404.install
    @@ -0,0 +1,96 @@
    + * Add redirect_404 table.
    + */
    +function redirect_404_update_8104() {
    +  $schema = [
    

    We don't need an update anymore. All sites will have to enable the new module, which will install the table.

  4. +++ b/modules/redirect_404/redirect_404.services.yml
    @@ -0,0 +1,10 @@
    +  redirect.not_found_storage:
    +    class: Drupal\redirect_404\RedirectNotFoundStorage
    

    lets add the backend_overridable tag here, see https://www.drupal.org/node/2306083

  5. +++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,94 @@
    +      if (!$this->configFactory->get('log_404')) {
    +        return;
    

    that means we don't need this check and also don't have to inject config.

  6. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,128 @@
    +      '#empty' => $this->config('redirect_404.settings')->get('log_404') ? $this->t('No 404 pages without redirects found.') : $this->t('404 requests are currently not logged, enable it in the Settings.'),
    

    this conditional can go away as well then, just use the first version.

  7. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,79 @@
    +   * Gets the plain database query object for the redirect_404 table as a list.
    ...
    +   * @return \Drupal\Core\Database\Query\SelectInterface $query
    +   *   The plain result object of the redirect_404 table query.
    

    That's not true anymore.

  8. +++ b/redirect.install
    @@ -45,7 +45,6 @@ function redirect_update_8100(&$sandbox) {
     }
     
    -
     /**
    

    unrelated change.

  9. +++ b/redirect.module
    @@ -39,6 +39,11 @@ function redirect_hook_info() {
     
    +  // Add redirect_404_update onto the end of hooks[] if its module is enabled.
    +  if (\Drupal::moduleHandler()->moduleExists('redirect_404')) {
    +    array_push($hooks, 'redirect_404_update');
    +  }
    

    no idea what this is :) There is no such hook.

  10. +++ b/redirect.services.yml
    @@ -13,7 +13,7 @@ services:
           - { name: event_subscriber }
       redirect.settings_cache_tag:
    -        class: Drupal\redirect\EventSubscriber\RedirectSettingsCacheTag
    -        arguments: ['@cache_tags.invalidator']
    -        tags:
    -          - { name: event_subscriber }
    +    class: Drupal\redirect\EventSubscriber\RedirectSettingsCacheTag
    +    arguments: ['@cache_tags.invalidator']
    +    tags:
    +      - { name: event_subscriber }
    diff --git a/src/Entity/Redirect.php b/src/Entity/Redirect.php
    

    Valid fix, but unrelated.

  11. +++ b/src/Entity/Redirect.php
    @@ -1,8 +1,4 @@
     <?php
    -/**
    - * @file
    - * Contains \Drupal\redirect\Entity\Redirect.
    - */
    

    same

  12. +++ b/src/Form/RedirectForm.php
    @@ -1,10 +1,5 @@
     
    -/**
    - * @file
    - * Contains \Drupal\redirect\Form\RedirectForm.
    - */
    -
    

    same. We should not have any changes anymore in redirect except removing code.

  13. +++ b/src/Form/RedirectSettingsForm.php
    @@ -55,6 +50,18 @@ class RedirectSettingsForm extends ConfigFormBase {
    +
    +    // Add 404 checkbox to the settings form if redirect_404 module is enabled.
    +    if (\Drupal::moduleHandler()->moduleExists('redirect_404')) {
    

    this can go too.

tduong’s picture

Status: Needs review » Needs work

The last submitted patch, 41: 404_language_aware-1559310-41.patch, failed testing.

The last submitted patch, 41: 404_language_aware-1559310-41.patch, failed testing.

Berdir’s picture

+++ b/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
@@ -0,0 +1,91 @@
+
+    // Check if we generate correct Add redirect url and if the form is
+    // pre-filled.
+    $destination = Url::fromUri('base:admin/config/search/redirect/404')->toString();
+    $this->assertUrl('admin/config/search/redirect/add', ['query' => ['path' => 'non-existing', 'langcode' => 'en', 'destination' => $destination]]);
+    $this->assertFieldByName('redirect_source[0][path]', 'non-existing');
+

You have those query arguments wrong.

See \Drupal\redirect\Form\RedirectForm::prepareEntity, you need to pass what's actually implemented. That means source and not path and language and not langcode.

tduong’s picture

Status: Needs work » Needs review

Updated "path --> source" and "langcode --> language" in both redirect_404 tests and RedirectFix404Form, removed debugs previously put for testing and reverted some @file doc leftover (issue about Redirect cleanups will be committed here: #2717103: Removing @file docblocks).

Running Fix404RedirectUILanguageTest locally, there is still testAutomaticRedirects (this is in RedirectUITest) that fails at $this->repository->findMatchingRedirect('term_test_alias');, it complaints about $this->repository not being an object, but findMatchingRedirect() has been called several times before like that and there are problems only here (?)... Let's see what happens in testbot now...

tduong’s picture

Status: Needs review » Needs work

The last submitted patch, 46: 404_language_aware-1559310-45.patch, failed testing.

The last submitted patch, 46: 404_language_aware-1559310-45.patch, failed testing.

tduong’s picture

tduong’s picture

Discussed with @miro_dietiker, we should drop the path that has already a redirect assigned form the Fix 404 pages list. Here I've just started adding the field to the redirect_404 table. Still work in progress...

miro_dietiker’s picture

I reviewed the situation and defined:
- We should clean up the table, similar to dblog, based on cron: remove records with smallest count and oldest hit
- 404's that are resolved (aka adding a redirect) should disappear. Thus we add a "resolved" column
-- Hitting a record that was previously marked as resolved, removes the flag again (happens when a redirect is deleted)
-- Thus we add a resolved column
- The admin UI should be built with a view
-- With language as an exposed filter
- Yeah, the UI should not link to an inexisting path
Thus, we have low complexity and the new 404 module is focussed to its purpose with a good experience.

+++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
@@ -70,10 +70,13 @@
+      if ($result->resolved) {

Should use a view (see above) and should be excluded with the filter when doing the query.

Finally, we have identified some follow-ups that possibly need discussions first. Please create the follow-up issues when we are near completion.

miro_dietiker’s picture

We need to decide if we still want to maintain a non-views fallback like #2716883: Make views dependency optional
To avoid the double complexity, we usually drop it and depend to views...

Also, do we need / can we add an option to disable the regular 404 dblog records to avoid record duplication?

Follow-up: Drupal can operate multiple domains and for 404's it's important to also know about the domain it originated.
(Domains could also be bound to languages through language negotiation.)

tduong’s picture

After discussions, debug, fixing stuff, ... finally there is a patch...
What is done now:
2) 404's that are resolved (aka adding a redirect) should disappear.
-a- Hitting a record that was previously marked as resolved, removes the flag again (happens when a redirect is deleted)
-b- Thus we add a resolved column
4) Yeah, the UI should not link to an inexisting path

Next step, view (reason: more flexible custom modifications):
3) The admin UI should be built with a view
-a- With language as an exposed filter

As what we've discussed yesterday, this is part of the followup:
1) We should clean up the table, similar to dblog, based on cron: remove records with smallest count and oldest hit

miro_dietiker’s picture

Status: Needs review » Needs work

Awesome progress and great to already have test coverage!

The views conversion is important for us, because we need capability to clone the view and offer a varying 404 tab with specific filters.

From reviewing the situation, i think the dblog cleanup with cron can not be a follow-up. Because currently dblog can be cleaned up and doesn't fill the DB.
But with the new module, the DB is filled more and more with every 404. That's a regression that could end up in a critical issue if bots are scanning your website with invalid URLs...

slashrsm’s picture

From reviewing the situation, i think the dblog cleanup with cron can not be a follow-up. Because currently dblog can be cleaned up and doesn't fill the DB.
But with the new module, the DB is filled more and more with every 404. That's a regression that could end up in a critical issue if bots are scanning your website with invalid URLs...

Agreed. Not just bots. It could also be used for a DoS attack.

The views conversion is important for us, because we need capability to clone the view and offer a varying 404 tab with specific filters.

@berdir mentioned in #23 that we might want to implement different backends (this is a perfect fit for Redis or MongoDB's capped collections). Views become a bit problematic in that case...

  1. +++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,80 @@
    +   * Constructs a new RedirectNotFoundStorage.
    

    Wrong class name in the comment.

  2. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,101 @@
    +      ->fields([
    +        'timestamp' => REQUEST_TIME,
    +        'count' => 1,
    +        'resolved' => 0,
    +      ])
    

    Should we create an index on this? It should speed up writes, specially in case of large datasets.

  3. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,101 @@
    +  public function updateLogRequest($path, $langcode) {
    

    This seems a bit unfortunate name of the function to me. It is not updating the log item but marking it as resolved.

  4. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,101 @@
    +      ->orderByHeader($header)
    ...
    +    $results = $query->condition('resolved', 0, '=')->execute()->fetchAll();
    

    Should we index fields that can be used to sort? At least count, which is selected by default?

  5. +++ b/modules/redirect_404/config/schema/redirect_404.schema.yml
    @@ -0,0 +1,9 @@
    +      label: '404 error database logs to keep'
    
    +++ b/modules/redirect_404/redirect_404.install
    @@ -0,0 +1,53 @@
    +    'primary key' => ['path', 'langcode'],
    

    .

  6. +++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,80 @@
    +   * Constructs a new RedirectNotFoundStorage.
    

    Wrong class name.

  7. CodeSniffer reports some things:
    ➜  redirect git:(8.x-1.x) ✗ phpcs --standard=Drupal --extensions=php,module,inc,install,test,profile,theme,js,css,info,txt,md modules/redirect_404 
    
    FILE: ...croot/modules/redirect/modules/redirect_404/redirect_404.install
    ----------------------------------------------------------------------
    FOUND 1 ERROR AFFECTING 1 LINE
    ----------------------------------------------------------------------
     4 | ERROR | [x] The second line in the file doc comment must be
       |       |     "@file"
    ----------------------------------------------------------------------
    PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
    ----------------------------------------------------------------------
    
    
    FILE: ...redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
    ----------------------------------------------------------------------
    FOUND 6 ERRORS AFFECTING 5 LINES
    ----------------------------------------------------------------------
      15 | ERROR | [ ] Missing short description in doc comment
      20 | ERROR | [ ] Missing short description in doc comment
      25 | ERROR | [ ] Missing short description in doc comment
      78 | ERROR | [ ] If the line declaring an array spans longer than
         |       |     80 characters, each element should be broken into
         |       |     its own line
      78 | ERROR | [ ] If the line declaring an array spans longer than
         |       |     80 characters, each element should be broken into
         |       |     its own line
     108 | ERROR | [x] Expected 1 newline at end of file; 2 found
    ----------------------------------------------------------------------
    PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
    ----------------------------------------------------------------------
    
    
    FILE: .../modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    ----------------------------------------------------------------------
    FOUND 5 ERRORS AFFECTING 4 LINES
    ----------------------------------------------------------------------
      19 | ERROR | [ ] Missing short description in doc comment
      60 | ERROR | [x] Missing function doc comment
      84 | ERROR | [ ] If the line declaring an array spans longer than
         |       |     80 characters, each element should be broken into
         |       |     its own line
      84 | ERROR | [ ] If the line declaring an array spans longer than
         |       |     80 characters, each element should be broken into
         |       |     its own line
     134 | ERROR | [x] Expected 1 newline at end of file; 2 found
    ----------------------------------------------------------------------
    PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
    ----------------------------------------------------------------------
    
    
    FILE: ...es/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php
    ----------------------------------------------------------------------
    FOUND 2 ERRORS AND 1 WARNING AFFECTING 3 LINES
    ----------------------------------------------------------------------
      8 | WARNING | [x] Unused use statement
     11 | ERROR   | [x] Missing class doc comment
     91 | ERROR   | [x] Expected 1 space after "=>"; 0 found
    ----------------------------------------------------------------------
    PHPCBF CAN FIX THE 3 MARKED SNIFF VIOLATIONS AUTOMATICALLY
    ----------------------------------------------------------------------
    
miro_dietiker’s picture

Discussed about pluggability.
When looking at past, we did past as API module defining the logging interface and event capture and past_db that does the db storage, plus offering the UI.
Current code is simple and database centric. As long as we are using the DB, i think we should following the core pattern / direction of using views as much as possible.

Once generalisation is done, we can reintroduce all the complexity of pluggability and configurability. For the time bing i think the only possibly healthy piece would be to rely to a StorageInterface in the EventSubscriber and not the Storage itself.

tduong’s picture

Sorry for the late update, discussions and supports took me a while...

For now, cleanuped code pointed by @shlashrsm (1/6, 3, 7). 2 and 4 has been discussed: in our case here, it is important to write/store our records fast. indexes make it faster to search tables, but longer to write to. having unused indexes (we would use it only to easily sort and select the records to cleanup) will end causing some unnecessary slow down. thus to have a fast write operation, we should not have indexes here.
As discussed, we will need the view thing first, so next: views! :D

tduong’s picture

tduong’s picture

So, I've tried to find out how to provide our redirect_404 fields and filters to use in a view, but still don't get where I need to say "use redirect_404 table" without an entity.
Currently there is only the Redirect view setting, so only the redirect table is accessible, and only User can be selected as a relationship, which cannot be used to reach our redirect_404 table.

For now I've just provided Path and Language field/filter, and Add Redirect button field plugins, and a ViewsData class.
I need some help to continue working on this, but let's discussed it tomorrow :)

tduong’s picture

Found it! Now I'm using a hook_views_data() and a wizard plugin to provide our fields and filters. Still work in progress, but here there is some first steps (not clean version).
Now I'm focusing on the operation buttons.

Maybe already one question: how can I convert a langcode (from the db) to a language to be displayed in the view ?

tduong’s picture

Yay! So now we have a nice view I think :D
Still need to clean up some code and add a test for our view filters. And the "dblog cleanups".

miro_dietiker’s picture

Status: Needs review » Needs work

Looks really nice. Some minor feedback.

  1. +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,43 @@
    +class Language extends LanguageField {
    

    I'm irritated, there is a language column in admin/content and core should provide a field handler already.

  2. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,78 @@
    +      $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
    +      $query->condition('path', $wildcard, 'LIKE');
    

    This looks non-D8-alike and risky. Plz recheck for a nice pattern.

    What irritates me is that preg_replace() seems to get wrong parameters. Does this do anything real?

slashrsm’s picture

  1. +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,119 @@
    +    'field' => '',
    +    'title' => t('Something'),
    +    'help' => t('Contains a list of something.'),
    

    It needs to be populated.

  2. +++ b/modules/redirect_404/src/Plugin/views/field/AddRedirect.php
    @@ -0,0 +1,70 @@
    +    if (\Drupal::entityTypeManager()->getAccessControlHandler('redirect')->createAccess()) {
    ...
    +    return \Drupal::service('renderer')->render($operations);
    

    Inject.

  3. +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,43 @@
    +  public function query() {
    +    $this->ensureMyTable();
    +
    +    $fields = [
    +      'redirect_404_langcode' => [
    +        'table' => 'redirect_404',
    +        'field' => 'langcode'
    +      ],
    +    ];
    +    $this->addAdditionalFields($fields);
    +  }
    

    This shouldn't be needed. Since the key in views data matches the name of the column in DB it should work automatically.

  4. +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,43 @@
    +    $langcode = $this->getValue($values, 'redirect_404_langcode');
    

    You don't need to provide field if you're getting the default one.

  5. +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,43 @@
    +    $language = ConfigurableLanguage::load($langcode)->label();
    

    ConfigurableLanguage is in language module. We either need to specify it as a dependency or find a way to make it optional. Maybe display a raw langcode if class does not exist?

  6. +++ b/modules/redirect_404/src/Plugin/views/wizard/Redirect404.php
    @@ -0,0 +1,37 @@
    +/**
    + * Defines a wizard for the watchdog table.
    + *
    + * @ViewsWizard(
    + *   id = "redirect_404",
    + *   module = "redirect_404",
    + *   base_table = "redirect_404",
    + *   title = @Translation("Redirect 404")
    + * )
    + */
    +class Redirect404 extends WizardPluginBase {
    

    Wrong comment.

  7. +++ b/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
    @@ -0,0 +1,48 @@
    +/**
    + * Defines a common interface for redirect 404 classes.
    + */
    +interface RedirectNotFoundStorageInterface {
    

    Comment seems wrong.

  8. +++ b/src/Form/RedirectForm.php
    @@ -142,6 +142,14 @@ class RedirectForm extends ContentEntityForm {
    +    // Update 'resolved' field in redirect_404 table, if its module is enabled.
    +    $path = '/' . $this->requestStack->getCurrentRequest()->query->get('source');
    +    $langcode = $this->requestStack->getCurrentRequest()->query->get('language');
    

    Shouldn't we get those two from the redirect entity?

tduong’s picture

Done points from #63.1-7.
#63.8 I need to do it like that because otherwise we will get the added redirect selected language instead of the original recorded one, which needs to match in the db.

Still need to do points of #62 after lunch and add tests.

tduong’s picture

Added view to be installed when the module is enabled, added test for Path and Language filter (not working now), and cleanup a bit of code. Will continue next week :)

Status: Needs review » Needs work

The last submitted patch, 65: 404_language_aware-1559310-65.patch, failed testing.

The last submitted patch, 65: 404_language_aware-1559310-65.patch, failed testing.

tduong’s picture

Added small feature (in the 404 page, display only the languages of the paths that are still resolved = 0), improved the tests.

Status: Needs review » Needs work

The last submitted patch, 68: 404_language_aware-1559310-68.patch, failed testing.

The last submitted patch, 68: 404_language_aware-1559310-68.patch, failed testing.

tduong’s picture

Added cron job to clean up redirect_404 rows (temporary set row_limit to 3 for test).

About the failing tests, why is testbot getting these /checkout from Url::fromUri('base:admin/config/search/redirect/404')->toString() ?

Status: Needs review » Needs work

The last submitted patch, 71: 404_language_aware-1559310-71.patch, failed testing.

The last submitted patch, 71: 404_language_aware-1559310-71.patch, failed testing.

miro_dietiker’s picture

Just a partial quick review.

  1. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,90 @@
    +function _redirect_404_get_langcodes() {
    +  $results = db_query('SELECT DISTINCT(langcode) FROM {redirect_404} WHERE resolved = :resolved ORDER BY langcode', ['resolved' => 0])
    
    +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,92 @@
    +      'options callback' => '_redirect_404_get_langcodes',
    

    This is very expensive. Note that we don't have a langcode index.
    With views, it's common that options display ALL possible values and not just what has data. If you want this, you use something like facets and an index that can power it efficiently.
    Nice idea, but let's switch to just list all languages.

  2. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,90 @@
    +      ->orderBy('count', 'DESC')
    +      ->orderBy('timestamp', 'DESC')
    ...
    +      \Drupal::database()->delete('redirect_404')
    +        ->condition('timestamp', $query, '<')
    +        ->execute();
    

    This doesn't work.

    Say the limit is 10 and 9 items have a count >1 and the 10th item has just the most recent hit with a count = 1. You would delete ALL data.

  3. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,201 @@
    +        'destination' => $destination,//ltrim($destination, '/'),
    

    hmm? and there are many similar commented rows. :-)

tduong’s picture

1. reverted condition and fixed the test.
2. I was about to follow your hint but they said that in MySQL is not allowed to delete the table used in a subquery, so I thought to write something like that, but sure it is not fine...
3. From tests fail in #68 I thought it was because of that, so I've tried to just give the $destination without any ltrim() but apparently is not that either... reverted. I guess it will still fail :/

Status: Needs review » Needs work

The last submitted patch, 75: 404_language_aware-1559310-75.patch, failed testing.

The last submitted patch, 75: 404_language_aware-1559310-75.patch, failed testing.

Berdir’s picture

+++ b/modules/redirect_404/redirect_404.module
@@ -0,0 +1,90 @@
+ */
+function redirect_404_cron() {
+  // Cleanup the redirect_404 table.
+  $row_limit = \Drupal::config('redirect_404.settings')->get('row_limit');
+
+  // For row limit n, get the wid of the nth row in descending wid order.
+  // Counting the most recent n rows avoids issues with wid number sequences,
+  // e.g. auto_increment value > 1 or rows deleted directly from the table.
+  if ($row_limit > 0) {
+    $query = \Drupal::database()->select('redirect_404', 'r404')
+      ->extend('Drupal\Core\Database\Query\TableSortExtender')
+      ->orderBy('count', 'DESC')
+      ->orderBy('timestamp', 'DESC')
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
+      ->limit($row_limit - 1)
+      ->fields('r404', ['timestamp'])
+      ->execute()->fetchField();
+
+    // Delete all table entries older than the nth row, if nth row was found.
+    if ($query) {
+      \Drupal::database()->delete('redirect_404')
+        ->condition('timestamp', $query, '<')
+        ->execute();

I guess this is taken from dblog_cron(), which has a unique id which works better there.

Adding such an id would not help us, since we update records and don't have a strict order based on the ID.

Instead of row count, we could instead define an age and then delete records that were not accessed anymore since N days. Could still fill the logs if you do a huge a mount of requests over a short time, but the data we write does not use a lot of space and you'll have other problems too if you have such a high amount of requests over a short time?

That said, to be honest, I really don't care if we delete a bunch of entries a second too late or too early.

However, the logic needs to be a method in the storage service (purgeOld() or so)

About the views, views has a system for preventing to install default config if the storage for it is not present. See \Drupal\views\Entity\View::isInstallable(), if the views data is not there, the view is not installed. So what you can do is make sure that you only define your views data when the service is using our specific implementation (\Drupal::service('...') instanceof OurClassName).

tduong’s picture

I'm closed to the solution, but there is still something trivial that leads me to more complex situation...

Since we've tried with
delete from redirect_404 where (path, langcode) in (select path, langcode from redirect_404 order by count desc, timestamp desc limit 2, 1);
and got the error
Error This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery’
so we've ended up to something that would simplify how to sort the rows and drop the irrelevant ones.

The idea now is to provide an extra field, relevancy, which value is updated based on the visit frequency of a redirect_404 record (radioactivity exponential decay concept). Given the formula $new_energy = $old_energy * $decay and $decay = pow(2, - $elapsed / $halflife) (N(t) = N_0 * e^(-t/T)), in our case we have:

$elapsed: REQUESTED_TIME - (last timestamp stored for this request)
$halflife: i've set it as a day per seconds (60*60*24 = 86400), otherwise if it is smaller it decay faster, but this is tbd

We thought that this $decay should be multiplied to the relevancy of the requested record, but we want to "downgrade" all the other to "give" more relevance to this requested row, so:

$old_energy: last relevancy stored for all the relevancy != this request (1: max relevancy)

I've tried to do this in a subquery and get that value to update all the other rows in the Sequel Pro console:

update redirect_404 set relevancy=(
  relevancy*(select pow(2, -(UNIX_TIMESTAMP(NOW()) - IFNULL(timestamp, UNIX_TIMESTAMP(NOW())))/86400 ) as decay
  from redirect_404
  where path='/test0' and langcode='en'))
where timestamp NOT IN
(select timestamp from redirect_404 where path='/test0' and langcode='en');

but it is not working (same as delete): You can't specify target table 'redirect_404' for update in FROM clause...

At the moment the uploading interdiff has only the new field schema and some attempts in RedirectNotFoundStorage::logRequest() to store/update relevancy. Once this works I'll move the cleanup logic in this service.

About the view, how/where should I create that method/check without the entity ?

Status: Needs review » Needs work

The last submitted patch, 79: 404_language_aware-1559310-79.patch, failed testing.

The last submitted patch, 79: 404_language_aware-1559310-79.patch, failed testing.

tduong’s picture

Other possible way, something with MySQL triggers ?

Berdir’s picture

I think you're overcomplicating this. I'll repeat my comment from above:

That said, to be honest, I really don't care if we delete a bunch of entries a second too late or too early.

What you are describing sounds like a ton of complexity, slower writes on 404 requests and more complicated delete queries.

IMHO, your only problem in the first attempt was to sort by count first. Just don't do that. Get the oldest timestamp (timestamp is updated on each new request) after the amount of entries that should be kept, then delete everything of the same age and older.

AFAICS, there are only two problems with that:

1. We might keep 1003 records instead of the configured 1000 because record 1000-1003 happend in the same second. So what? On a site that is that busy, there are likely going to be new requests while we're doing this delete, so it will already have more anyway. Doesn't matter.
2. On a very busy site, with thousands of unique 404 requests, more important 404's with multiple requests might get deleted. I expect we'll set the default to 10k or so. That means more than 10k requests on different 404's need to happen to delete data for such a case. That's quite a lot, and if you're worried about that, just set it to 100k or so. We're storing very little data, much less than a watchdog entry, so storing 10 or 100k entries really isn't an issue.

miro_dietiker’s picture

Yeah, thx for trying to move forward with this idea @tduong.

Not sure if the energy / relevancy pattern is a good idea. I was just thinking out loud.
Here's the missing piece: The relevancy value is only updated when the row is updated. Thus, the relevancy stored is the relevancy at the time of the timestamp.

With this pattern, you usually never need to do bulk updates to those tables.
Instead you should order by calculating the current relevancy, something like:
ORDER BY pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp))/86400 DESC

IMHO the complexity looks pretty limited with this approach and we can filter out irrelevant things easily.
All other tries failed. We are open for better approaches.

(We are only using the DB to store raw data and have business logic in PHP. Triggers and stored procedures is not how Drupal usually does it. Also i don't see how it could simplify things here... ;-) )

miro_dietiker’s picture

That was a cross post with Berdir.

Note that when a bot like Google scans your site, every 404 page is receiving a single hit in a period.
If you have these kind of bots active and still over time cumulated items with currently very low frequency, you risk to loose them.
I agree to simplify, we can cutoff based on large limit numbers with timestamp order.
I discussed with tramy to figure out how much we can simplify the approach and if it's worth considering. If it's complicated it's not and we most likely can live with the limitation.

tduong’s picture

Couldn't make it work even with two queries. Tomorrow I will try with the timestamp and make it work.

Status: Needs review » Needs work

The last submitted patch, 86: 404_language_aware-1559310-86.patch, failed testing.

The last submitted patch, 86: 404_language_aware-1559310-86.patch, failed testing.

tduong’s picture

Finally it works, but only using query() instead of select() and delete(). Still need to clean up some code and move the cron logic in the storage service, after lunch :P

Status: Needs review » Needs work

The last submitted patch, 89: 404_language_aware-1559310-89.patch, failed testing.

The last submitted patch, 89: 404_language_aware-1559310-89.patch, failed testing.

tduong’s picture

Uh, something to mention: the relevancy values are stored wrong now because the most 404 page visited is *decreasing* instead of increasing, but I don't think that doing $relevancy = $relevancy + $relevancy * $decay is a good solution because it grows too fast (?)

And also in the views.view.redirect_404.yml I've commented some lines because it breaks the view, but as soon as someone export the view, he/she gets these lines enabled again... Not sure how to fix it (count numeric in views data) and if doing what @Berdir suggested above will avoid this bug...

miro_dietiker’s picture

Yeah we missed the hit +1 here:
$relevancy = $relevancy * $decay + 1

Some quick reviews again. :-)

  1. +++ b/modules/redirect_404/config/install/redirect_404.settings.yml
    @@ -0,0 +1 @@
    +row_limit: 3
    

    I think we should ship a better default. :-)

  2. +++ b/modules/redirect_404/config/install/views.view.redirect_404.yml
    @@ -0,0 +1,533 @@
    +          content: 'No 404 pages without redirects found.'
    

    Double negation makes my mind melt. How about "There are no 404 errors to fix."

  3. +++ b/modules/redirect_404/redirect_404.install
    @@ -0,0 +1,60 @@
    +        'not null' => TRUE,
    +        'description' => 'The path of the request',
    ...
    +        'description' => 'The language of this request',
    ...
    +        'not null' => TRUE,
    

    I would prefer a standard order of the keys.

  4. +++ b/modules/redirect_404/redirect_404.install
    @@ -0,0 +1,60 @@
    +        'description' => 'The number of requests with that path and language',
    

    (multiple) Missing final dots in description in schema.

  5. +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,92 @@
    +      'options callback' => '_redirect_404_get_langcodes',
    

    Back again? Thought you confirmed removal of the language distinct query?

  6. +++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,81 @@
    +    debug($this->redirectStorage->purgeOldRequests());
    

    We should not purge on exception. And don't do a debug either. :-)

  7. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,130 @@
    +class RedirectFix404Form extends FormBase {
    

    So we drop this whole class finally?
    (otherwise view would need to go into optional...)
    Please don't forget to add the dependency to views!

tduong’s picture

Status: Needs work » Needs review
FileSize
16.38 KB
65.87 KB

Oook, now there is something that works! :D

Discussions, several attempts and then finally fixed the cleanup cronjob, cleaned up some code, refactored points mentioned above.
About 6., it was just for me, to test/see the result (debug is too slow), and I've uploaded the patch in case there was someone that wanted to review it :P
Point 7., now I've added a @todo for that form and listRequest(). We will discuss it later or in a follow up :)
For now I still have the view in install. Didn't check if the tests pass if it is in /optional. I'll check it tomorrow.

Unfortunately there is something wrong in the KernelTest that I've just written after the cronjob fix: somehow out purgeOldRequests() method in redirect_404_cron() is not triggered. Anyone knows why and how to make it work ?

(...go to watch the match :D)

Status: Needs review » Needs work

The last submitted patch, 94: 404_language_aware-1559310-94.patch, failed testing.

The last submitted patch, 94: 404_language_aware-1559310-94.patch, failed testing.

miro_dietiker’s picture

+++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
@@ -0,0 +1,104 @@
+    $select = $this->database->query('SELECT path, langcode, (relevancy * pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp)/' . $this->halflife . ')) as cutoff FROM {redirect_404} ORDER BY (relevancy * pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp)/' . $this->halflife . ')) DESC LIMIT ' . $row_limit . ', 1')->fetchObject();
...
+    $this->database->query('DELETE FROM {redirect_404} WHERE (relevancy * pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp)/' . $this->halflife . ')) < :cutoff', [':cutoff' => $select->cutoff]);

If the table is empty or even if there are less records than limit, this looks to me like it will fail fatal. Also $select seems a bad name for a row - plus neither path nor langcode are needed here.

miro_dietiker’s picture

IMHO the added complexity by this pattern is minimalistic. The functional part is 2 lines of code compared to timestamp sorting (plus schema lines). But we should add comments about the math so people can understand it. :-)

tduong’s picture

#97: again, it was just for testing as a "mid-solution". Sorry for uploading dirty patches ^^'

I'll post a comment with the explanation first, so next the uploading patch will be on the top of the list in the IS.
Now the Redirect 404 view works fine, and also the filters (our Language filter takes the default Drupal language filter) (see screenshots "Fix 404 pages view" and "view filters"), after assigning a redirect to a record ("add redirect") it appears in the Redirect page ("redirect"), if you look for that path in the Fix 404 page it is not displayed anymore since it is marked as resolved ("redirect assigned").

Mysql math performance information:
We've tested with ~400'000 rows through drush sql-cli command line.
The time it took to run the select query to order by the effective relevancy at the current time was 2.69 sec (the same query with the limit condition to get the cutoff value should last more or less the same because it has to run through every row to compute the math expression anyway but it doesn't have to display each rows, so we got 1.09 sec for this), then for the delete query it took 3.06 sec (see the first 2 screenshots).

tduong’s picture

Now this patch should be clean enough :P
- moved views.view.redirect_404.yml in /optional, and added check in redirect_404_views_data() to only define our views data when the service is using our specific implementation
- cleaned up some codes (in RedirectFix404Form there are only array syntax refactoring)
- added comment/math explanation in the RedirectNotFoundStorage doc block, and some further comments in some methods and test
- cleaned/shortened/break in multiple lines some queries that were too long (hope it doesn't hurt the readability too much ^^')
- fixed web and kernel tests

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/modules/redirect_404/config/install/redirect_404.settings.yml
    new file mode 100644
    index 0000000..d86d593
    
    index 0000000..d86d593
    --- /dev/null
    
    --- /dev/null
    +++ b/modules/redirect_404/config/optional/views.view.redirect_404.yml
    

    it doesn't have to be optional config. there are no additional dependences, so it can't ever not be installed, except when the storage is not there, but that happens on a different level.

  2. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,52 @@
    +/**
    + * @file
    + * Clean redirect_404 from irrelevant rows to keep a limited amount of records.
    + */
    

    That's not the description of the file but from the purge old requests method.

    This should simply say something like "Module file for redirect_404."

  3. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,52 @@
    +/**
    + * @defgroup redirect_api Redirection API
    + * @{
    + * Functions related to URL redirects.
    + *
    + * @} End of "defgroup redirect_api".
    + */
    

    remove this.

  4. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,52 @@
    +function redirect_404_form_system_logging_settings_alter(&$form, FormStateInterface $form_state) {
    +  $row_limits = [100, 1000, 10000, 100000, 1000000];
    +  $form['dblog_row_limit'] = [
    +    '#type' => 'select',
    +    '#title' => t('404 error database logs to keep'),
    +    '#default_value' => \Drupal::configFactory()->getEditable('redirect_404.settings')->get('row_limit'),
    +    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
    +    '#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
    +  ];
    +
    +  $form['#submit'][] = 'redirect_404_logging_settings_submit';
    

    wondering what makes more sense, having this in the redirect settings or in the loggin settings. Not sure...

  5. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,52 @@
    +  \Drupal::configFactory()->getEditable('redirect_404.settings')->set('row_limit', $form_state->getValue('dblog_row_limit'))->save();
    

    use chaining to make this more readable

    \Drupal::configFactory()
    ->getEditable()
    ->set()
    ->save()

  6. +++ b/modules/redirect_404/redirect_404.routing.yml
    @@ -0,0 +1,7 @@
    +redirect.fix_404:
    +  path: '/admin/config/search/redirect/404'
    +  defaults:
    +    _title: 'Fix 404 pages'
    +    _form: '\Drupal\redirect_404\Form\RedirectFix404Form'
    +  requirements:
    +    _permission: 'administer redirects'
    

    not sure if we really need bot the view and the controller?

  7. +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,96 @@
    +  $data = [];
    +  if (!\Drupal::service('redirect.not_found_storage') instanceof RedirectNotFoundStorage) {
    +    return $data;
    

    a comment for this would be good.

  8. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,135 @@
    +/**
    + * Provides a form that lists all 404 error paths and no redirect assigned yet.
    + *
    + * Keep this form, if we want to implement different backends to better fit
    + * Redis or MongoDB's capped collections. Otherwise drop this class and the
    + * RedirectNotFoundStorage::listRequests() method. See discussion in #1559310
    + * comment #55.
    + */
    +class RedirectFix404Form extends FormBase {
    

    ah, it was discussed here.

    I would avoid "we", try to write it from a third person (In case someone uses an alternative backend).

  9. +++ b/modules/redirect_404/src/Plugin/views/field/AddRedirect.php
    @@ -0,0 +1,120 @@
    +  public function render(ResultRow $values) {
    +    $link = [];
    +    if ($this->entityTypeManager->getAccessControlHandler('redirect')->createAccess()) {
    +      $query = [
    +        'query' => [
    +          'source' => ltrim($this->getValue($values, 'redirect_404_path'), '/'),
    +          'language' => $this->getValue($values, 'redirect_404_langcode'),
    

    field plugins have a special access method, then it won't show up at all, this would give a column with empty and possibly broken operations.

  10. +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,28 @@
    +  public function render(ResultRow $values) {
    +    $langcode = $this->getValue($values);
    +    $language = ConfigurableLanguage::load($langcode)->label();
    +
    

    You should use getLanguage() from the language manager. otherwise you might get problems with UND or deleted languages. Even then, make sure you really get a language object back, in case a language gets deleted.

  11. +++ b/modules/redirect_404/src/Plugin/views/wizard/Redirect404.php
    @@ -0,0 +1,32 @@
    + */
    +class Redirect404 extends WizardPluginBase {
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function defaultDisplayOptions() {
    +    $display_options = parent::defaultDisplayOptions();
    +
    +    // Add permission-based access control.
    +    $display_options['access']['type'] = 'perm';
    +    $display_options['access']['options']['perm'] = 'administer redirects';
    +
    +    return $display_options;
    +  }
    

    not sure if having a wizard for this is worth it, only very, very few people will create additional new views.

  12. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,120 @@
    +/**
    + * Controller class for redirect not found storage.
    + *
    

    this is not a controller: Sql implementation for ...

    Might make sense to also include that part in the class name: SqlRedirectNotFoundStorage

  13. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,120 @@
    +    $cutoff_exp = '(relevancy * pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp)/86400.00))';
    +    $cutoff = $this->database
    +      ->query('SELECT ' . $cutoff_exp . ' as cutoff FROM {redirect_404} ORDER BY ' . $cutoff_exp . ' DESC LIMIT ' . $row_limit . ', 1')
    +      ->fetchField();
    

    does ORDER BY cutoff not work?

    limit like this is not standard compliant I think.

    it would be better to use a select() query with addExpression()

  14. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,120 @@
    +    // Delete records below cutoff value, if given. Otherwise skip the cleanup.
    +    if (!empty($cutoff)) {
    +      $this->database
    +        ->query('DELETE FROM {redirect_404} WHERE ' . $cutoff_exp . ' < ' . $cutoff);
    +    }
    

    same here, even though it is safe here, never put dynamic variables directly into queries if it can be avoided. this should be easy with ->delete() and a condition?

  15. +++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
    @@ -0,0 +1,120 @@
    +      // Replace wildcards with PDO wildcards.
    +      // @todo Find a way to write a nicer pattern, if we want to implement
    +      // different backends to better fit Redis or MongoDB's capped collection,
    +      // thus keep Drupal\redirect_404\Form\RedirectFix404Form. Otherwise drop
    +      // the form and this method. See discussion in #1559310 comment #55.
    +      $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
    

    a nicer pattern for what?

    comment seems very repetitive to the one on the class and doesn't tell me much. This pattern was used in 7.x as well, not sure why we have to replace it or how that is related to redis/mongodb.

  16. +++ b/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
    @@ -0,0 +1,54 @@
    +   * @param string $search
    +   *   The search text.
    

    this should contain documentation about which characters are expected to be treated as wildcards.

  17. +++ b/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
    @@ -0,0 +1,55 @@
    +    // Change timestamp to 1000000 more for the first 6 rows to get different
    +    // relevancy so we won't delete all rows when we run cron.
    +    db_query("UPDATE {redirect_404} SET timestamp='1466082407' WHERE path BETWEEN '/test0' AND '/test5';");
    +    redirect_404_cron();
    

    queries should not have ; at the end.

    this should use db_update(), and this doesn't seem to be a sufficient test for the relevancy stuff.

    I'd expect to see a test that simulates a situtation where a record with a higher count and older timestamp would get deleted first, but the relevancy prevents that.

    Also the between check is too much magic for me, I'd expect the scenario above will require multiple, specific update queries (one with count 1/relevancy NOW() - 600 and timestamp x, one with count 5 and timestamp xy - 1200 and so on.

  18. +++ b/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
    @@ -0,0 +1,55 @@
    +    // Check that now we have 6 rows in the redirect_404 table.
    +    $result = db_query("SELECT COUNT(*) as rows FROM {redirect_404};")->fetchObject();
    +    $this->assertEquals(6, $result->rows);
    

    you can use fetchField to get a single value.

  19. +++ b/src/Form/RedirectForm.php
    @@ -142,6 +142,14 @@ class RedirectForm extends ContentEntityForm {
       public function save(array $form, FormStateInterface $form_state) {
    +    // Update 'resolved' field in redirect_404 table, if its module is enabled.
    +    $path = '/' . $this->requestStack->getCurrentRequest()->query->get('source');
    +    $langcode = $this->requestStack->getCurrentRequest()->query->get('language');
    +    $redirect = $form_state->getValue(['redirect_redirect', 0]);
    +    if (\Drupal::moduleHandler()->moduleExists('redirect_404') && !empty($redirect['uri'])) {
    +      \Drupal::service('redirect.not_found_storage')->resolveLogRequest($path, $langcode);
    +    }
    +
    

    might be possible to do this in redirect_404 instead. redirects are an entity which trigger an hook_redirect_insert() hook. you can listen to that and then call this there.

tduong’s picture

Points 1-3, 5-10, 12, and 18 done. Further feedback:
4: if we want to move this into redirect, then the not found storage service needs to be moved too, otherwise if the user does not enable the redirect_404 sub-module the service call in hook_cron() won't work, will it ? I guess it is fine to leave it there.

11: not fully sure how this magic works, but afaik without this wizard plugin we don't point the queries to any table. after past discussions with @miro_dietiker, he said we don't need an entity for redirect_404, so I'm not sure where else we can tell our view to look into the redirect_404 table.

15: from @miro_dietiker comment #62

+++ b/modules/redirect_404/src/RedirectNotFoundStorage.php
@@ -0,0 +1,78 @@
+      $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
+      $query->condition('path', $wildcard, 'LIKE');

This looks non-D8-alike and risky. Plz recheck for a nice pattern.

What irritates me is that preg_replace() seems to get wrong parameters. Does this do anything real?

19: how ?

Currently doing: 13-17

tduong’s picture

Misread point 4. forget about what i wrote above.
Otherwise is there already any decision about what to do for these points left ?

In this patch:
13.-14. edited queries.
16. added comment for wildcard, but i guess i still need to improve it (?)
17. done. about the extended test, i've just updated the relevancy field in one line and afaik a test should not have too many randomised values, otherwise it hurts readability (?) also added helper methods to specifically assert which rows are there and which are not. also added the $resolved parameter in case we will need it for other tests in the future.
19. hook added, fixed related code.

Also noticed:
Instead of getting the path with $this->currentPath, we should use $this->requestStack to allow storing the arguments as well (tested with /foo?foo=1 and /foo?foo=2, with the previous code only /foo is stored for both cases), but now if we have /de/foo?foo=3, it stores the langcode correctly, but in path it is stored as /de/foo?foo=3. I will check/fix it tomorrow.

Status: Needs review » Needs work

The last submitted patch, 103: 404_language_aware-1559310-103.patch, failed testing.

The last submitted patch, 103: 404_language_aware-1559310-103.patch, failed testing.

tduong’s picture

Uploading patch:

  1. Improved how to get the langcode from the redirect_404_redirect_presave(): when a redirect has been assigned, in case you had a 404 path in 'en' and you want the redirect to be in 'de', if you use the redirect entity to get the langcode you will get 'de', but you want 'en' because you ve stored that path in 'en'. with the previous code then you wont be able to mark this path to resovled because "un-matching langcode". changed it.
  2. After discussion with @Berdir, instead of just using $this->currentPath or $this->requestStack, we've combined both of them to get the cleaned path and append the (evtl.) URL's arguments.
  3. Fixed the UItests, improved and extended them to cover also paths with URL arguments.
  4. Improved the kerneltest to actually test the "order by" hardcoding every row's relevancy

Status: Needs review » Needs work

The last submitted patch, 106: 404_language_aware-1559310-106.patch, failed testing.

The last submitted patch, 106: 404_language_aware-1559310-106.patch, failed testing.

tduong’s picture

Discussed with @Berdir about #106.1: redirecting to another language does not resolve the path. if the user wants to have a redirect to another language, that should be up to the user, not our problem. thus reverted it and fixed the Fix404RedirectUILanguageTest.

About the failing tests, calling $redirect->getSourceUrl() then it calls toString() which generates a different Url string for testbot. now this should be fixed (added a new method in Redirect.php to get the source url with its arguments), but i leave the debug statements in the test to evtl. see what else is the bug. thus, this is a dirty patch!

Refactored Redirect404Subscriber::onKernelException() to the D8 std.

Status: Needs review » Needs work

The last submitted patch, 109: 404_language_aware-1559310-109.patch, failed testing.

The last submitted patch, 109: 404_language_aware-1559310-109.patch, failed testing.

tduong’s picture

Forgot to fixed a test line.

tduong’s picture

Berdir’s picture

Status: Needs review » Needs work

Last round of cleanup.

Lets discuss this first.

  1. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,60 @@
    +  $form['dblog_row_limit'] = [
    +    '#type' => 'select',
    +    '#title' => t('404 error database logs to keep'),
    +    '#default_value' => \Drupal::configFactory()->getEditable('redirect_404.settings')->get('row_limit'),
    +    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
    +    '#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
    +  ];
    

    dblog_row_limit is a weird name, has nothing to do with dblog. Should be redirect_404_row_limit.

    Also, we still need to discuss if this should be in the redirect settings or this form.

  2. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,60 @@
    +  // Get the original langcode of this path, not the redirect one.
    +  $langcode = $redirect->get('language')->value;
    

    wrong comment, no longer true. Just remove it.

  3. +++ b/modules/redirect_404/redirect_404.module
    @@ -0,0 +1,60 @@
    +
    +  // Update 'resolved' field in redirect_404 table.
    +  \Drupal::service('redirect.not_found_storage')->resolveLogRequest($path, $langcode);
    

    Too much technical details. It's a service, someone could write a service that uses redis as storage. Then there is no table.

    Suggestion: Mark a potentially existing log entry for this path as resolved.

  4. +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,98 @@
    +
    +  $data['redirect_404']['table']['group'] = t('Redirect 404');
    +  $data['redirect_404']['table']['wizard_id'] = 'redirect_404';
    

    Lets look at this wizard thing together. There has to be a better way IMHO.

    I don't see one in statistics_views_data() for example

  5. +++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
    @@ -0,0 +1,96 @@
    +    // Log 404 only exceptions.
    +    if ($event->getException() instanceof NotFoundHttpException) {
    

    That doesn't sound like proper english:

    Only log page not found (404) errors.

  6. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,135 @@
    + * Keep this form, if they want to implement different backends to better fit
    + * Redis or MongoDB's capped collections. Otherwise drop this class and the
    + * SqlRedirectNotFoundStorage::listRequests() method. See discussion in
    + * #1559310 comment #55.
    + */
    +class RedirectFix404Form extends FormBase {
    

    I still don't think this comment will work.

    Otherwise to what? Either we actually have a @todo to do something, with an issue. Or we simply document it as a matter of fact.

    Which would be something like this:

    This is a fallback for the provided default view.

  7. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,135 @@
    +    if ($multilingual) {
    +      $header[] = ['data' => $this->t('Language'), 'field' => 'language'];
    +    }
    

    This is something that the view won't do yet, correct?

    We can actually make it, by implementing access() with the same logic.

  8. +++ b/modules/redirect_404/src/Plugin/views/field/AddRedirect.php
    @@ -0,0 +1,126 @@
    + * Provides a views field for the 'Add redirect' button.
    + *
    + * @ingroup views_field_handlers
    + *
    + * @ViewsField("add_redirect")
    + */
    +class AddRedirect extends FieldPluginBase {
    

    We discussed a second operation for later: Manually resolve/ignore a path.

    So, lets rename this to RedirectOperations (also for machine names, keys, ..), then we can easily add more operations later.

  9. +++ b/modules/redirect_404/src/Plugin/views/field/AddRedirect.php
    @@ -0,0 +1,126 @@
    +  public function query() {
    +    $this->ensureMyTable();
    +
    +    $fields = [
    +      'redirect_404_langcode' => [
    +        'table' => 'redirect_404',
    +        'field' => 'langcode'
    +      ],
    +      'redirect_404_path' => [
    +        'table' => 'redirect_404',
    +        'field' => 'path'
    +      ]
    +    ];
    +    $this->addAdditionalFields($fields);
    

    Have a look at \Drupal\views\Plugin\views\field\FieldPluginBase::init. I think we can put this into the views data definition, then we don't need this method.

  10. +++ b/modules/redirect_404/src/SqlRedirectNotFoundStorageInterface.php
    @@ -0,0 +1,54 @@
    +/**
    + * Interface for redirect 404 services.
    + */
    +interface SqlRedirectNotFoundStorageInterface {
    

    The interface is *not* SQL specific. Only our implementation is. So.. no Sql prefix.

  11. +++ b/modules/redirect_404/src/SqlRedirectNotFoundStorageInterface.php
    @@ -0,0 +1,54 @@
    +  /**
    +   * Cleans the irrelevant data stored in the redirect_404 table.
    +   */
    +  public function purgeOldRequests();
    

    Same here and in other cases here. As far as the interface is concerned, there is no redirect_404 table. Use a generic description instead, like 404 requests log.

  12. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,222 @@
    +    // Add predefined language.
    +    $this->drupalPostForm('admin/config/regional/language/add', ['predefined_langcode' => 'fr'], t('Add language'));
    +    $this->assertText('French');
    

    we add french here and the other languages above?

  13. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,222 @@
    +    $body = '/html/body/div/main/div/div/div/div/div[2]/table/tbody';
    +    $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), 'French'));
    

    there is no reason to specify every single tag here. It will break for no reason. Especially since all you do then is just look for "French" in the whole table?

    What is that for exactly? If that's all you do, why not just do an assertText()?

  14. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,222 @@
    +    $this->assertOption('edit-langcode', 'All');
    +    $this->assertOption('edit-langcode', '***LANGUAGE_site_default***');
    +    $this->assertOption('edit-langcode', '***LANGUAGE_language_interface***');
    +    $this->assertOption('edit-langcode', 'en');
    +    $this->assertOption('edit-langcode', 'de');
    +    $this->assertOption('edit-langcode', 'es');
    +    $this->assertOption('edit-langcode', 'fr');
    +    $this->assertOption('edit-langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
    +    $this->assertOption('edit-langcode', LanguageInterface::LANGCODE_NOT_APPLICABLE);
    +    $this->clickLink(t('Add redirect'));
    

    Some of those options here don't make sense. This should only list:

    * All
    * unspecified (maybe labeled as "Any language")
    * the actual langauges..

    for implementation, it might be easiest to get the real languages and then just preprend the two others.

  15. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,222 @@
    +    // Check the error path visit count.
    +    $xpath = '/html/body/div/main/div/div/div/div/div[2]/table/tbody/tr/td[2]';
    

    Same here. There's only one one table, //table/tbody/tr/td is specific enough.

  16. +++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
    @@ -0,0 +1,222 @@
    +    $this->drupalGet('admin/config/search/redirect/404');
    +    $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), 'French'));
    +    $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), 'English'));
    

    Same for those additional language checks. why use this for those but assertText() for everything else?

  17. +++ b/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
    @@ -0,0 +1,137 @@
    +    // Add some records in the redirect_404 table for test.
    +    for ($i = 0; $i < 10; $i++) {
    +      db_query("INSERT INTO {redirect_404} (path, langcode, count, timestamp, resolved, relevancy) VALUES (:path, :langcode, 1, 1465082407, 0, 1.00)", [
    +        ':path' => '/test' . $i,
    +        ':langcode' => 'en'
    +      ]);
    +    }
    +
    +    // Check that there are 10 rows in the redirect_404 table.
    +    $result = db_query("SELECT COUNT(*) as rows FROM {redirect_404}")->fetchField();
    +    $this->assertEquals(10, $result);
    +
    +    // Update/hardcode the relevancy for each row to not delete all rows after
    +    // running the cronjob.
    +    db_update('redirect_404')->fields(['relevancy' => '91.97781'])->condition('path', '/test5')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '82.63037'])->condition('path', '/test6')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '71.79651'])->condition('path', '/test2')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '67.85126'])->condition('path', '/test7')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '53.98305'])->condition('path', '/test4')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '42.81542'])->condition('path', '/test3')->execute();
    +    // The following rows have smaller relevancy than the cutoff value defined
    +    // by the row_limit and will be dropped.
    +    db_update('redirect_404')->fields(['relevancy' => '37.99983'])->condition('path', '/test9')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '28.99964'])->condition('path', '/test1')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '15.79511'])->condition('path', '/test0')->execute();
    +    db_update('redirect_404')->fields(['relevancy' => '3.929704'])->condition('path', '/test8')->execute();
    +    redirect_404_cron();
    

    Getting there :)

    Why dont you just do 10 db_insert() queries in the first place instead of a loop and then update them?

tduong’s picture

Done as suggested. Points 1, 6 need discussion, 13, 14, 16, 17 still need work, i ll do it tomorrow.

Status: Needs review » Needs work

The last submitted patch, 115: 404_language_aware-1559310-115.patch, failed testing.

The last submitted patch, 115: 404_language_aware-1559310-115.patch, failed testing.

tduong’s picture

Fixed and cleaned code in the Fix404RedirectUITest and Fix404RedirectUILanguageTest, created Redirect404TestBase AssertRedirectTrait which are imported in the 404 webtests, and cleaned Fix404RedirectCronJobTest kerneltest as suggested.

Still have points 1 and 6 left.

Berdir’s picture

+++ b/modules/redirect_404/src/Tests/Redirect404TestBase.php
@@ -0,0 +1,143 @@
+    'dblog',

dblog should no longer be required now.

Otherwise I think we're done, with the exception of the last two points there that we need to discuss with @miro_dietiker.

tduong’s picture

tduong’s picture

Fixed last points (1 and 6), changed that docblock in the RedirectFix404Form and moved the cron settings into the redirect settings.
Sorted! :D

Berdir’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests
+++ b/modules/redirect_404/redirect_404.module
@@ -20,25 +21,28 @@
- * Implements hook_form_FORM_ID_alter() for system_logging_settings().
+ * Implements hook_form_alter().
  */
-function redirect_404_form_system_logging_settings_alter(&$form, FormStateInterface $form_state) {
-  $row_limits = [100, 1000, 10000, 100000, 1000000];
-  $form['redirect_404_row_limit'] = [
-    '#type' => 'select',
-    '#title' => t('404 error database logs to keep'),
-    '#default_value' => \Drupal::configFactory()->getEditable('redirect_404.settings')->get('row_limit'),
-    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
-    '#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
-  ];
+function redirect_404_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  if ($form_id == 'redirect_settings_form') {
+    $row_limits = [100, 1000, 10000, 100000, 1000000];

too much change, you can implement this just like before, with a FORM_ID based alter. you just need to do a search replace from one form id to the other.

tduong’s picture

tduong’s picture

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Ok, I'm out of things to complain about.

I think @larowlan plans to review/test this soon as well.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

One issue and some general observations

  1. +++ b/modules/redirect_404/redirect_404.routing.yml
    @@ -0,0 +1,7 @@
    +redirect.fix_404:
    

    Redirect module still has a menu local task pointing to this, it need to be moved too. See redirect.links.task.yml

  2. +++ b/modules/redirect_404/redirect_404.views.inc
    @@ -0,0 +1,99 @@
    +  if (!\Drupal::service('redirect.not_found_storage') instanceof SqlRedirectNotFoundStorage) {
    

    Should this typehint a new interface instead of the concrete implementation?

  3. +++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
    @@ -0,0 +1,132 @@
    +    $languages = \Drupal::languageManager()->getLanguages(LanguageInterface::STATE_ALL);
    +    $multilingual = \Drupal::languageManager()->isMultilingual();
    ...
    +    $redirect_storage = \Drupal::service('redirect.not_found_storage');
    ...
    +      $row['timestamp'] = \Drupal::service('date.formatter')->format($result->timestamp, 'short');
    ...
    +      if (\Drupal::entityTypeManager()->getAccessControlHandler('redirect')->createAccess()) {
    
    +++ b/modules/redirect_404/src/Plugin/views/field/Language.php
    @@ -0,0 +1,24 @@
    +    return \Drupal::languageManager()->isMultilingual();
    

    nit:These could all be injected

  4. +++ b/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
    @@ -0,0 +1,120 @@
    +    $row_limit = \Drupal::config('redirect_404.settings')->get('row_limit');
    

    There's a hidden dependency here that could be injected too

miro_dietiker’s picture

Yay, awesome. :-)

Just for sake of completeness, potential follow-ups discussed are:
- Implement a redis backend, because it seems to be a great fit and is popular in D8 enterprise environments, at least for us.
- Add an "ignore" action on a 404 record that makes it hide permanently, even if repeating again.
- Figure out how we can get rid off the watchdog 404 entries.

Berdir’s picture

1. Missed that, nice find. Yes, that needs to be fixed.
2. That's on purpose. The views data is tied to that specific implementation, a different implementation, even if it is sql based, might have a different table structure, then they need to provide their own views integration.
3. They could, but we're more or less just moving that form around. But yeah, lets fix it while we're touching it.

tduong’s picture

Fixed as suggested. About point 1, moved 404 related stuff from redirect.links.menu.yml as well.

Status: Needs review » Needs work

The last submitted patch, 129: 404_language_aware-1559310-129.patch, failed testing.

The last submitted patch, 129: 404_language_aware-1559310-129.patch, failed testing.

tduong’s picture

Forgot to update the route name in other places..

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Inject all the things :)

larowlan’s picture

Gah, I thought this was committed and filed a new issue for an issue it creates #2780985: Ignore 404 paths longer than the maximum column length

Do we want to leave this RTBC and postpone the other one?

miro_dietiker’s picture

Hm, i would update this issue here and we can quickly review and RTBC it, so we have a healthy single feature issue.

No reply by Dave since 4 months. Hope we are still aligned with his expectations.
I also updated the issue about co-maintaining redirect: #2778701: Offer to co-maintain the redirect 8.x branch

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
84.46 KB
3.15 KB

Ok - here is combined patch.

Status: Needs review » Needs work

The last submitted patch, 136: lang-aware-1559310.136.patch, failed testing.

The last submitted patch, 136: lang-aware-1559310.136.patch, failed testing.

The last submitted patch, 136: lang-aware-1559310.136.patch, failed testing.

Berdir’s picture

+++ b/src/SqlRedirectNotFoundStorage.php
@@ -47,6 +53,12 @@ public function __construct(Connection $database) {
+    if (Unicode::strlen($path) > static::MAX_PATH_LENGTH) {
+      // Don't attempt to log paths that would result in an exception. There is
+      // no point in logging truncated paths, as they cannot be used to build a
+      // new redirect.
+      return;
+    }

this was something that we discussed. we can't make the key longer and keep it as primary/unique key.

Path may include query arguments, there is an argument to be made that we could cut them off. Or add an additional column and store the full path there.

Looks like the new test is missing a constructor argument?

But that's something that we can do later. For now, the most important part is to not throw an exception.

Berdir’s picture

Ah, I guess you wrote the patch against an older version?

toncic’s picture

Fixed test failing.

Status: Needs review » Needs work

The last submitted patch, 142: 404_pages_should_be-1559310-142.patch, failed testing.

The last submitted patch, 142: 404_pages_should_be-1559310-142.patch, failed testing.

toncic’s picture

And interdiff.

toncic’s picture

Fixed type mismatch.

larowlan’s picture

The patch size seems to have doubled?

Berdir’s picture

Status: Needs review » Needs work

Yeah, there's a patch in there again, only checked the interdiffs...

miro_dietiker’s picture

Please clean your patch, it is double the size of the previous #136
You again uploaded a patch in a patch - see lang-aware-1559310.136.patch
I recommend to never use a "git add -A" or "*" and also scroll through every patch you create before you upload it.

toncic’s picture

Cleaning patch. I hope that is better now.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Now it looks good again.

miro_dietiker’s picture

Just for completeness, providing interdiff to #133 when this was RTBC before.

Berdir’s picture

Status: Reviewed & tested by the community » Needs work
FileSize
652 bytes

We had a problem on one site which receives a lot of invalid/weird input, including invalid UTF-8.

This resulted in a lot of exceptions. The attached interdiff adds a check to skip those. please update the patch and extend our unit test coverage for this case.

tduong’s picture

Updated patch including the patch above. Forgot about the test, I'll do it asap.

Discussed with @Berdir and he was considering a new feature (allow the user to set some paths to be ignored by the 404 redirect error listener) and was wondering if it should be in a followup, but he would like to have this in as soon as possible, so added this new feature + test.

Status: Needs review » Needs work

The last submitted patch, 154: 404_language_aware-1559310-154.patch, failed testing.

The last submitted patch, 154: 404_language_aware-1559310-154.patch, failed testing.

tduong’s picture

Added a test for the UTF-8 thingy. Still need to figure out why the previous patch doesn't work and improve the test_only patch (but local test passes...).

Status: Needs review » Needs work

The last submitted patch, 157: 404_language_aware-1559310-157.patch, failed testing.

The last submitted patch, 157: 404_language_aware-1559310-157.patch, failed testing.

Berdir’s picture

+++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
@@ -81,6 +92,17 @@
 
+      // Ignore paths specified in the redirect settings.
+      if ($pages = $this->config->get('pages')) {
+        $pages = array_map('trim', explode("\n", $pages));
+        foreach ($pages as $page) {
+          $page = array_map('trim', explode("*", $page));
+          if (strpos($this->currentPath->getPath(), reset($page))) {
+            return;
+          }
+        }
+      }

we should use the path matcher service here, $this->pathMatcher->matchPath($path_alias, $pages) as RequestPath does.

Berdir’s picture

The test-only patch shows by not failing that the test isn't executed at all. phpunit tests currently need to be in top-level tests folder or they are not executed by testbot.

I also think my feedback, has not been addressed yet. Also, the default config and schema must cover the existing setting too.

tduong’s picture

Done as suggested above (comment #160).

tduong’s picture

New patch including the deleted file lines, since it doesn't apply otherwise.

The last submitted patch, 162: 404_language_aware-1559310-161.patch, failed testing.

The last submitted patch, 162: 404_language_aware-1559310-161.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 163: 404_language_aware-1559310-163.patch, failed testing.

The last submitted patch, 163: 404_language_aware-1559310-163.patch, failed testing.

tduong’s picture

Fixed test.

tduong’s picture

Discussed with @Berdir and he would like to add another feature here:

- add an "Ignore" operation in the "Fix 404 pages" page
- when "Ignore" an entry, mark this record as resolved, 'temporary page' to show what is happening (with path and langcode as query arguments), redirect to the Settings page, and add the deleted entry into the "Pages to ignore" textarea
- display a message saying that the entry has been deleted and ask to save the settings (so that the user could evtl. edit the new path to ignore)

Started implementing the controller. Still need to figure out how to add the Ignore operation in a view.inc file. I'll check it tomorrow.

miro_dietiker’s picture

@Berdir, @tduong Is this only about static pathes as exact matches, or will we also apply globbing to match wildcards in excludes?

Berdir’s picture

Status: Needs review » Needs work

It supports wildcards.

  1. +++ b/modules/redirect_404/src/Controller/Fix404Ignore.php
    @@ -0,0 +1,70 @@
    +  public function fix404IgnorePath($path, $langcode) {
    

    I think just ignorePath is enough here.

  2. +++ b/modules/redirect_404/src/Controller/Fix404Ignore.php
    @@ -0,0 +1,70 @@
    +    $path_to_ignore = ($langcode != $default_langcode) ? ($langcode . '/' . $path) : $path;
    

    the language should not be part of the path to ignore, we strip that off from the language before we match it, no?

  3. +++ b/modules/redirect_404/src/Controller/Fix404Ignore.php
    @@ -0,0 +1,70 @@
    +      $config = $this->configuration->getEditable('redirect_404.settings');
    +      $config->set('pages', $ignored_paths);
    +      $config->save();
    +
    +      drupal_set_message($this->t('Added the path %path into the ignored list.', [
    +        '%path' => $path_to_ignore,
    +      ]));
    

    No, don't save it here. Pass it along as a query parameter to the settings page, like ?ignore=some/path.

    Then in the form UI, read that, and append it. For example, the user might want to convert it into a wildcard or so.

tduong’s picture

Done as suggested, feature still in progress. Renamed the controller to "Fix404IgnoreController" and it results as a new file.

tduong’s picture

Ok, fixed last leftovers here:
To be able to save the 'Path to ignore' configurations, the form names had to be changed and improved the test coverage for this 'Ignore' feature.

Berdir’s picture

+++ b/modules/redirect_404/src/Controller/Fix404IgnoreController.php
@@ -64,7 +64,7 @@
-    if (!strpos($path, $ignored_paths)) {
+    if (empty($ignored_paths) || (!empty($ignored_paths) && !strpos($path, $ignored_paths))) {
       $this->redirectStorage->resolveLogRequest($path, $langcode);

the !empty isn't required· PHP is intelligent enough to not execute the part after || if the first condition returns true and if the first condition is FALSE, then the second is always true.

tduong’s picture

Berdir’s picture

Tested the ignoring, the UI works well. However, the wildcard matching only worked for the first line. That's because you tried to unify the paths by removing the / on both, what you need to do is to have exactly one leading / on the incoming path.

The attached patch worked for me in manual testing. We need to check why our tests are not failing on this. Maybe we only tested the first pattern?

Status: Needs review » Needs work

The last submitted patch, 176: 404_language_aware-1559310-176.patch, failed testing.

The last submitted patch, 176: 404_language_aware-1559310-176.patch, failed testing.

tduong’s picture

The problem is because in the test the stored paths to be ignored are without leading slash, which then don't properly match in the listener class (Redirect404Subscriber) and don't actually ignore the pages.

Fixed the test and added a check in redirect_404_logging_settings_submit() to make sure that we store the 'pages to ignore' with leading slash.

Also changed a check in Redirect404Subscriber::onKernelException() to be consistent and have the same thing as Drupal\system\Plugin\Condition\RequestPath::evaluate()

Do not trim a trailing slash if that is the complete path.

instead of "leading".

Berdir’s picture

Status: Needs review » Needs work

One part that we are missing is hook_help().

#2725577: Improve redirect_help() updated the help text, we need to figure out a way to handle this. We could make it dynamic, and show a text to enable the optional submodule to do that if it's not enabled and the user has permission to do so. Or just move that part to redirect_404_help().

Berdir’s picture

Also, 99% sure this needs a reroll after my recent commits.

tduong’s picture

Rerolled and added dynamic text for redirect_404 in redirect_help():

1) submodule enabled: show description, link to "Fix 404 pages" page
2) submodule not enabled, user has permission: show description, link to "Extend" page to enable it
3) submodule not enabled, user doesn't have permission: don't show anything about "Fix 404 pages" in /admin/help/redirect

Opted for the dynamic text because otherwise we should place 2) in redirect_help() and add redirect_404_help() only to place 1) since atm we don't have/need any help text for redirect_404.

Berdir’s picture

Status: Needs review » Needs work

As discussed, lets add a update function to enable the module.

tduong’s picture

Added update function to enable the submodule.
Renamed the last update function committed 2 days ago, fixed it wrapping its content in a check, as an exception message would appear (entity ID 'redirect_delete_action' already exists).

Tested locally, it works (supervised by @Berdir).

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Ok, I think this is it.

Will commit this soon.

The last submitted patch, 153: redirect-invalid.patch, failed testing.

The last submitted patch, 153: redirect-invalid.patch, failed testing.

  • Berdir committed dbd3992 on 8.x-1.x authored by tduong
    Issue #1559310 by tduong, miro_dietiker: 404 pages should be language...
Berdir’s picture

Version: 8.x-1.x-dev » 7.x-2.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Ok, committed!

We've been using this for months on big NP8 sites and it has been working well for us after those initial fixes about weird broken UTF-8 characters and lenght problems. The wildcards are pretty new but well enough tested I think.

  • Berdir committed d0f4759 on 8.x-1.x
    Issue #1559310 by Berdir: Moved 404 fix local action link to...
stella’s picture

Does the redirect_404 module have to have a dependency on the language module? I don't have a multilingual site, but I want the functionality of redirect_404 without having to turn that module on.

Berdir’s picture

I think that's an oversight, try to just remove that dependency. We use the language manager, but that's not in language.module. Patches welcome :)

stella’s picture

I patched the module to remove the dependency and disabled the language module, and didn't encounter any errors. Patch attached.

Berdir’s picture

Thanks, can you open a new issue for that, though? so we can propery let it be tested and credit you :)

stella’s picture

Berdir, I did wonder what was correct protocol here. New issue created for it at #2851810: redirect_404 module has a dependency on language module