At first I was tempted to alter menu items responsible for node auto-loader function like this:
/**
* Implements hook_menu_alter to hijack node auto-loader function.
*/
function et404_menu_alter(&$items) {
// Remove original node view item...
unset($items['node/%node']);
// ... add our own node view item with custom auto-loader
$items['node/%et404_hijacked_node'] = array(
// Array items are copy-paste from node.module
'title callback' => 'node_page_title',
'title arguments' => array(1),
// The page callback also invokes drupal_set_title() in case
// the menu router's title is overridden by a menu link.
'page callback' => 'node_page_view',
'page arguments' => array(1),
'access callback' => 'node_access',
'access arguments' => array('view', 1),
);
}
/**
* Overrides node_load to show 404 page if translation is not available.
*/
function et404_hijacked_node_load($nid = NULL, $vid = NULL, $reset = FALSE) {
global $user;
global $language;
$node = node_load($nid, $vid, $reset);
// Only anonymous users are refused access
if (!$user->uid && !isset($node->translations->data[$language->language])) {
$node = false;
}
return $node;
}
Unfortunately this solution won't work correctly, because of menu_get_object() function that is used across drupal. That function assumes that every entity has it's auto-loader function named like ENTITY_TYPE_load()
. The _load
suffix is hardcoded into the function whereas it should be red from the entity info array provided by the hook_entity_info() (explicitly the load hook
parameter).
I decided to use different approach that involves hook_node_access()
.
Actually instead of showing 404 with drupal_goto()
I decided to incorporate drupal core and show 403. It can be turned into 404 anyway with a help of m4032404 module.
Please note that the following code is part of a module that also provides language fallback features similar to those from language fallback module - hence the reference to $language->fallback
property that doesn't exists in vanilla D7. I am the author of language fallback patch for entity translation for D7 and D8 but for this particular project I needed something very sophisticated that I couldn't achieve with the language fallback module.
/**
* Implements hook_node_access()
*/
function et404_node_access($node, $op, $account = null) {
// We restrict access only to anonymous users so authenticated users can still manage content
if ($account->uid) {
return;
}
// Basic sanity check
if (!is_object($node)) {
return NODE_ACCESS_DENY;
}
global $language;
// Check if translation for current language exists
if (!isset($node->translations->data[$language->language])) {
// Check if translation for fallback language exists
if (LANGUAGE_NONE != $language->fallback && isset($node->translations->data[$language->fallback])) {
return;
}
return NODE_ACCESS_DENY;
}
}
/**
* Implements hook_translated_menu_link_alter()
*
* This function checks if specific menu item points to the node entity
* and if it is, checks if node has translation for current UI language.
* Nodes without translation are hidden.
*
* See StackExchange answer for more info:
* http://drupal.stackexchange.com/a/32024/5982
*/
function et404_translated_menu_link_alter(&$item, $map) {
// Alter only custom menu items from "main menu"
if ('menu' != $item['module'] || !isset($item['menu_name']) || 'main-menu' != $item['menu_name']) {
return;
}
// Process only nodes
if (!isset($map[1]) || !isset($map[0]) || 'node' != $map[0]) {
return;
}
if(!is_object($map[1])) {
// Instead of loading node manually, use core function for that.
if (!_menu_load_objects($item, $map)) {
return;
}
}
// After _menu_load_objects() the $map[1] contains node object
$entity = &$map[1];
if (!et404_translation_check_entity($entity)) {
// There is no translation for this entity in current UI language.
// Hide the item and quit.
$item['hidden'] = true;
return;
}
}
/**
* Implements hook_menu_link_alter()
*
* When custom menu item is saved, we add 'alter' option to be able to
* customize this item with hook_translated_menu_link_alter() implementation.
* See et404_translated_menu_link_alter() for details.
*
* @param array $item
* @param array $map
*/
function et404_menu_link_alter(&$item, $map) {
// Alter only custom menu items from "main menu"
if (isset($item['module']) && 'menu' == $item['module'] && isset($item['menu_name']) && 'main-menu' == $item['menu_name']) {
$item['options']['alter'] = TRUE;
}
}
/**
* Check if there is a translation for entity in current UI language only
* @param object $entity
* @return boolean|string
* FALSE if translation is NOT available; otherwise true.
* Note that FALSE is returned also for entities without "translations" property.
*/
function et404_translation_check_entity($entity) {
// Invalid data?
if (empty($entity)) {
return false;
}
global $language;
// Check if current UI language is the same as entity language
// This is a fail-safe in case "translations" property is missing
// Note that taxonomy terms doesn't have language property
if (!empty($entity->language) && $entity->language == $language->language) {
return true;
}
// Check if "translations" property exists.
// Without it further checks doesn't make sense
if (empty($entity->translations)) {
return false;
}
// Check if there is a current UI language translation available.
if (!empty($entity->translations->data[$language->language])) {
return true;
}
// Check if there is a fallback language translation available.
if (LANGUAGE_NONE != $language->fallback && !empty($entity->translations->data[$language->fallback])) {
return true;
}
// No translation available
return false;
}
And as a bonus, a fix for language switching links:
/**
* Implements hook_language_switch_links_alter
*
* Function alters language switching links for nodes
* if there is no translation for currently viewed node
*/
function et404_language_switch_links_alter(array &$links, $type, $path) {
$translations = array_keys($links);
$translations = array_flip($translations);
if (arg(0, $path) == 'node' && arg(1, $path) && !arg(2, $path)) {
// !arg(2, $path) --> This will make sure we're not altering links like /node/1/edit/LANG
// Most probably dealing with nodes
$node = node_load(arg(1, $path));
if ($node) {
$translations = array_keys($node->translations->data);
$translations = array_flip($translations);
}
}
$languages = language_list();
foreach($links as $lang => &$link) {
// Check if translation for the language exists
if (!isset($translations[$lang])) {
// If not, check if the fallback translation is also missing.
if (!isset($languages[$lang]->fallback) | LANGUAGE_NONE != $languages[$lang]->fallback && isset($translations[$languages[$lang]->fallback])) {
continue;
}
$link['href'] = '<front>';
}
}
}