Issue #2900421 by timmillwood, tim.plunkett, catch, Sam152: Architectural review of the Content Moderation module
parent
98c5df8941
commit
22b8c33521
|
@ -129,9 +129,6 @@ function content_moderation_form_alter(&$form, FormStateInterface $form_state, $
|
|||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK().
|
||||
*
|
||||
* Many default node templates rely on $page to determine whether to output the
|
||||
* node title as part of the node content.
|
||||
*/
|
||||
function content_moderation_preprocess_node(&$variables) {
|
||||
\Drupal::service('class_resolver')
|
||||
|
@ -287,7 +284,11 @@ function content_moderation_workflow_insert(WorkflowInterface $entity) {
|
|||
* Implements hook_ENTITY_TYPE_update().
|
||||
*/
|
||||
function content_moderation_workflow_update(WorkflowInterface $entity) {
|
||||
content_moderation_workflow_insert($entity);
|
||||
// Clear bundle cache so workflow gets added or removed from the bundle
|
||||
// information.
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
// Clear field cache so extra field is added or removed.
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Drupal\content_moderation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessException;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
|
@ -81,10 +82,8 @@ class LatestRevisionCheck implements AccessInterface {
|
|||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* returns the Entity in question.
|
||||
*
|
||||
* @throws \Exception
|
||||
* A generic exception is thrown if the entity couldn't be loaded. This
|
||||
* almost always implies a developer error, so it should get turned into
|
||||
* an HTTP 500.
|
||||
* @throws \Drupal\Core\Access\AccessException
|
||||
* An AccessException is thrown if the entity couldn't be loaded.
|
||||
*/
|
||||
protected function loadEntity(Route $route, RouteMatchInterface $route_match) {
|
||||
$entity_type = $route->getOption('_content_moderation_entity_type');
|
||||
|
@ -94,7 +93,7 @@ class LatestRevisionCheck implements AccessInterface {
|
|||
return $entity;
|
||||
}
|
||||
}
|
||||
throw new \Exception(sprintf('%s is not a valid entity route. The LatestRevisionCheck access checker may only be used with a route that has a single entity parameter.', $route_match->getRouteName()));
|
||||
throw new AccessException(sprintf('%s is not a valid entity route. The LatestRevisionCheck access checker may only be used with a route that has a single entity parameter.', $route_match->getRouteName()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,10 +41,10 @@ class ContentPreprocess implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wrapper for hook_preprocess_HOOK().
|
||||
*
|
||||
* @param array $variables
|
||||
* Theme variables to preprocess.
|
||||
*
|
||||
* @see hook_preprocess_HOOK()
|
||||
*/
|
||||
public function preprocessNode(array &$variables) {
|
||||
// Set the 'page' template variable when the node is being displayed on the
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\content_moderation\ContentModerationStateInterface;
|
||||
use Drupal\Core\Entity\ContentEntityBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
|
@ -148,7 +147,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
|
|||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* A moderated entity object.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ContentModerationStateInterface|null
|
||||
* @return \Drupal\content_moderation\Entity\ContentModerationStateInterface|null
|
||||
* The related content moderation state or NULL if none could be found.
|
||||
*
|
||||
* @internal
|
||||
|
@ -172,7 +171,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
|
|||
->execute();
|
||||
|
||||
if ($ids) {
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
/** @var \Drupal\content_moderation\Entity\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = $storage->loadRevision(key($ids));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
|
@ -6,6 +6,8 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
|
||||
/**
|
||||
* Customizations for block content entities.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BlockContentModerationHandler extends ModerationHandler {
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
|||
* Common customizations for most/all entities.
|
||||
*
|
||||
* This class is intended primarily as a base class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInterface {
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
* Much of the logic contained in this handler is an indication of flaws
|
||||
* in the Entity API that are insufficiently standardized between entity types.
|
||||
* Hopefully over time functionality can be removed from this interface.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface ModerationHandlerInterface {
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
|||
|
||||
/**
|
||||
* Customizations for node entities.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class NodeModerationHandler extends ModerationHandler {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Routing;
|
||||
namespace Drupal\content_moderation\Entity\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
|
@ -3,6 +3,7 @@
|
|||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity;
|
||||
use Drupal\content_moderation\Entity\ContentModerationStateInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
@ -94,6 +95,8 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*
|
||||
* @see hook_entity_presave()
|
||||
*/
|
||||
public function entityPresave(EntityInterface $entity) {
|
||||
if (!$this->moderationInfo->isModeratedEntity($entity)) {
|
||||
|
@ -103,7 +106,8 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
if ($entity->moderation_state->value) {
|
||||
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
|
||||
/** @var \Drupal\content_moderation\ContentModerationState $current_state */
|
||||
$current_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value);
|
||||
$current_state = $workflow->getTypePlugin()
|
||||
->getState($entity->moderation_state->value);
|
||||
|
||||
// This entity is default if it is new, a new translation, the default
|
||||
// revision, or the default revision is not published.
|
||||
|
@ -113,13 +117,13 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
|| !$this->moderationInfo->isDefaultRevisionPublished($entity);
|
||||
|
||||
// Fire per-entity-type logic for handling the save process.
|
||||
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
|
||||
$this->entityTypeManager
|
||||
->getHandler($entity->getEntityTypeId(), 'moderation')
|
||||
->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
|
@ -133,8 +137,6 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
|
@ -217,8 +219,6 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being deleted.
|
||||
*
|
||||
|
@ -232,8 +232,6 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity revision being deleted.
|
||||
*
|
||||
|
@ -252,8 +250,6 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $translation
|
||||
* The entity translation being deleted.
|
||||
*
|
||||
|
@ -274,8 +270,6 @@ class EntityOperations implements ContainerInjectionInterface {
|
|||
/**
|
||||
* Act on entities being assembled before rendering.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_view()
|
||||
* @see EntityFieldManagerInterface::getExtraFields()
|
||||
*/
|
||||
|
|
|
@ -18,7 +18,7 @@ use Drupal\Core\StringTranslation\TranslationInterface;
|
|||
use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
|
||||
use Drupal\content_moderation\Routing\EntityModerationRouteProvider;
|
||||
use Drupal\content_moderation\Entity\Routing\EntityModerationRouteProvider;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
|
@ -121,8 +121,6 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
/**
|
||||
* Adds Moderation configuration to appropriate entity types.
|
||||
*
|
||||
* This is an alter hook bridge.
|
||||
*
|
||||
* @param EntityTypeInterface[] $entity_types
|
||||
* The master entity type list to alter.
|
||||
*
|
||||
|
@ -171,10 +169,6 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
/**
|
||||
* Gets the "extra fields" for a bundle.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_extra_field_info()
|
||||
*
|
||||
* @return array
|
||||
* A nested array of 'pseudo-field' elements. Each list is nested within the
|
||||
* following keys: entity type, bundle name, context (either 'form' or
|
||||
|
@ -193,6 +187,8 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
* - delete: (optional) String containing markup (normally a link) used as
|
||||
* the element's 'delete' operation in the administration interface. Only
|
||||
* for 'form' context.
|
||||
*
|
||||
* @see hook_entity_extra_field_info()
|
||||
*/
|
||||
public function entityExtraFieldInfo() {
|
||||
$return = [];
|
||||
|
@ -239,6 +235,8 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
*
|
||||
* @return \Drupal\Core\Field\BaseFieldDefinition[]
|
||||
* New fields added by moderation state.
|
||||
*
|
||||
* @see hook_entity_base_field_info()
|
||||
*/
|
||||
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
|
||||
if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
|
@ -251,7 +249,6 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
->setDescription(t('The moderation state of this piece of content.'))
|
||||
->setComputed(TRUE)
|
||||
->setClass(ModerationStateFieldItemList::class)
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setDisplayOptions('view', [
|
||||
'label' => 'hidden',
|
||||
'region' => 'hidden',
|
||||
|
|
|
@ -48,7 +48,7 @@ class ModerationStateFieldItemList extends FieldItemList {
|
|||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity the content moderation state entity will be loaded from.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ContentModerationStateInterface|null
|
||||
* @return \Drupal\content_moderation\Entity\ContentModerationStateInterface|null
|
||||
* The content_moderation_state revision or FALSE if none exists.
|
||||
*/
|
||||
protected function loadContentModerationStateRevision(ContentEntityInterface $entity) {
|
||||
|
@ -69,7 +69,7 @@ class ModerationStateFieldItemList extends FieldItemList {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
/** @var \Drupal\content_moderation\Entity\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = $content_moderation_storage->loadRevision(key($revisions));
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$langcode = $entity->language()->getId();
|
||||
|
|
|
@ -6,7 +6,6 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
|||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
@ -16,13 +15,6 @@ use Symfony\Component\Validator\ConstraintValidator;
|
|||
*/
|
||||
class ModerationStateConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The state transition validation.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
|
@ -42,13 +34,10 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
|||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validation
|
||||
* The state transition validation.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, StateTransitionValidation $validation, ModerationInformationInterface $moderation_information) {
|
||||
$this->validation = $validation;
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
@ -59,7 +48,6 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
|
|||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_moderation.state_transition_validation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
|||
* },
|
||||
* )
|
||||
*/
|
||||
class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPluginInterface {
|
||||
class ContentModeration extends WorkflowTypeBase implements ContentModerationInterface, ContainerFactoryPluginInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
|
@ -135,52 +135,28 @@ class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPlug
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the entity types the workflow is applied to.
|
||||
*
|
||||
* @return string[]
|
||||
* The entity types the workflow is applied to.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEntityTypes() {
|
||||
return array_keys($this->configuration['entity_types']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any bundles the workflow is applied to for the given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to get the bundles for.
|
||||
*
|
||||
* @return string[]
|
||||
* The bundles of the entity type the workflow is applied to or an empty
|
||||
* array if the entity type is not applied to the workflow.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBundlesForEntityType($entity_type_id) {
|
||||
return isset($this->configuration['entity_types'][$entity_type_id]) ? $this->configuration['entity_types'][$entity_type_id] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the workflow applies to the supplied entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to check.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workflow applies to the supplied entity type ID and bundle
|
||||
* ID. FALSE if not.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
return in_array($bundle_id, $this->getBundlesForEntityType($entity_type_id), TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an entity type ID / bundle ID from the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to remove.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to remove.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
if (!isset($this->configuration['entity_types'][$entity_type_id])) {
|
||||
|
@ -199,14 +175,7 @@ class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPlug
|
|||
}
|
||||
|
||||
/**
|
||||
* Add an entity type ID / bundle ID to the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to add. It is responsibility of the caller to provide
|
||||
* a valid entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to add. It is responsibility of the caller to provide a
|
||||
* valid bundle ID.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\WorkflowType;
|
||||
|
||||
use Drupal\workflows\WorkflowTypeInterface;
|
||||
|
||||
/**
|
||||
* Interface for ContentModeration WorkflowType plugin.
|
||||
*/
|
||||
interface ContentModerationInterface extends WorkflowTypeInterface {
|
||||
|
||||
/**
|
||||
* Gets the entity types the workflow is applied to.
|
||||
*
|
||||
* @return string[]
|
||||
* The entity types the workflow is applied to.
|
||||
*/
|
||||
public function getEntityTypes();
|
||||
|
||||
/**
|
||||
* Gets any bundles the workflow is applied to for the given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to get the bundles for.
|
||||
*
|
||||
* @return string[]
|
||||
* The bundles of the entity type the workflow is applied to or an empty
|
||||
* array if the entity type is not applied to the workflow.
|
||||
*/
|
||||
public function getBundlesForEntityType($entity_type_id);
|
||||
|
||||
/**
|
||||
* Checks if the workflow applies to the supplied entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to check.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workflow applies to the supplied entity type ID and bundle
|
||||
* ID. FALSE if not.
|
||||
*/
|
||||
public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
|
||||
/**
|
||||
* Removes an entity type ID / bundle ID from the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to remove.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to remove.
|
||||
*/
|
||||
public function removeEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
|
||||
/**
|
||||
* Add an entity type ID / bundle ID to the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to add. It is responsibility of the caller to provide
|
||||
* a valid entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to add. It is responsibility of the caller to provide a
|
||||
* valid bundle ID.
|
||||
*/
|
||||
public function addEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param $entity
|
||||
* Content Moderation uses this parameter to determine the initial state
|
||||
* based on publishing status.
|
||||
*/
|
||||
public function getInitialState($entity = NULL);
|
||||
|
||||
}
|
Loading…
Reference in New Issue