devportal-8.x-2.0-alpha10/modules/api_reference/devportal_api_reference.module
modules/api_reference/devportal_api_reference.module
<?php
/**
* @file
* Main module file for Devportal API Reference.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\devportal_api_reference\ReferenceInterface;
use Drupal\file\Entity\File;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\file\FileInterface;
use Drupal\node\NodeInterface;
use Drupal\taxonomy\Entity\Term;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides the list of API Reference related node bundles.
*/
const DEVPORTAL_API_REFERENCE_BUNDLES = ['api_reference'];
/**
* Implements hook_menu_links_discovered_alter().
*/
function devportal_api_reference_menu_links_discovered_alter(array &$links): void {
// Add menu links for API Reference bundles.
foreach (\Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple(DEVPORTAL_API_REFERENCE_BUNDLES) as $type) {
$type_id = $type->id();
$type_label = $type->label();
// Menu link for node add form.
$links["entity.api_ref.add.{$type_id}"] = [
'title' => t('Add @type', ['@type' => $type_label]),
'parent' => 'entity.api_ref.collection',
'route_name' => 'node.add',
'route_parameters' => [
'node_type' => $type_id,
],
];
// Menu link for node bundle configuration.
$links["entity.api_ref.configuration.{$type_id}"] = [
'title' => $type_label,
'parent' => 'system.admin_devportal_config',
'description' => t('Manage %type configuration.', ['%type' => $type_label]),
'route_name' => 'entity.node_type.edit_form',
'route_parameters' => [
'node_type' => $type_id,
],
];
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function devportal_api_reference_form_node_api_reference_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
_devportal_api_reference_allow_skip_upload_on_node_form($form, $form_state, TRUE);
}
/**
* Custom validation function api_reference node bundle.
*
* @param array $form
* The form array.
* @param Drupal\Core\Form\FormStateInterface $form_state
* The form_state object.
*/
function devportal_api_reference_api_reference_validate(array $form, FormStateInterface $form_state): void {
/** @var \Drupal\node\Entity\Node $node */
$node = $form_state->getFormObject()->getEntity();
$source = $form_state->getValue('field_source_file');
try {
if ($fid = ($source[0]['fids'][0] ?? NULL)) {
$file = File::load($fid);
if (!$file) {
\Drupal::messenger()->addError('An error occoured while saving the file.');
$form_state->setValue('field_source_file', []);
return;
}
/** @var \Drupal\devportal_api_reference\ReferenceInterface $type */
[, $version, , $doc, $type] = _devportal_api_reference_get_data_from_file($file->getFileUri());
if (devportal_api_reference_check_api_version($node, $version)) {
\Drupal::messenger()->addError('This version has been added before or missing.');
$form_state->setValue('field_source_file', []);
return;
}
$mappings = \Drupal::moduleHandler()
->invokeAll('devportal_api_reference_fields', [$type, $doc, $file]);
foreach ($mappings as $field_name => $value) {
$form_state->setValue($field_name, $value);
}
}
elseif ($oldfids = ($form_state->get('field_source_file_tmp')['fids'][0] ?? NULL)) {
// If no source file was uploaded use the last uploaded source file.
$source[0]['fids'][0] = $oldfids;
$form_state->setValue('field_source_file', $source);
}
}
catch (\Exception $e) {
watchdog_exception('devportal_api_reference', $e);
\Drupal::messenger()->addError($e->getMessage());
$form_state->setValue('field_source_file', []);
}
}
/**
* Implements hook_devportal_api_reference_fields().
*/
function devportal_api_reference_devportal_api_reference_fields(ReferenceInterface $type, \stdClass $doc, FileInterface $file, ?Request $request = NULL): array {
$description = (string) $type->getDescription($doc);
return [
'title' => [['value' => (string) $type->getTitle($doc)]],
'field_version' => [['value' => (string) $type->getVersion($doc)]],
'field_description' => [
[
'value' => $description,
'summary' => $description ? text_summary($description, 'github_flavored_markdown') : '',
'format' => 'github_flavored_markdown',
],
],
];
}
/**
* Submit callback for the api reference node form.
*
* This function adds the file version to the revision log message.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*/
function devportal_api_reference_api_reference_submit(array $form, FormStateInterface $form_state): void {
$key = ['revision_log', 0, 'value'];
$source = $form_state->getValue('field_source_file');
if (($fid = ($source[0]['fids'][0] ?? NULL)) && $form_state->getValue('revision')) {
/** @var \Drupal\file\Entity\File $file */
$file = File::load($fid);
[, $version] = _devportal_api_reference_get_data_from_file($file->getFileUri());
$message = $form_state->getValue($key);
$message .= ($message ? PHP_EOL . PHP_EOL : '') . t('Version: @version', [
'@version' => $version,
]);
$form_state->setValue($key, $message);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function devportal_api_reference_form_node_api_reference_edit_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
_devportal_api_reference_allow_skip_upload_on_node_form($form, $form_state, FALSE);
/** @var \Drupal\node\Entity\Node $node */
$node = $form_state->getFormObject()->getEntity();
// Check the triggering element. If the triggering element is a link the
// triggering element property will be set to NULL. If it is from an AJAX
// request (in this case clicking the 'browse' button and uploading a file) it
// will have an object assigned to it. By default set the default_value to
// NULL.
if ($form_state->getTriggeringElement() === NULL) {
$form_state->set('field_source_file_tmp', $form['field_source_file']['widget'][0]['#default_value']);
$form['field_source_file']['widget'][0]['#default_value'] = NULL;
}
// Get the revisions of this node, and create an array of the files contained
// in these revisions. This array will be used to populate the 'Previously
// uploaded files' fieldset.
$previous_files = devportal_api_reference_get_previous_files($node->id());
// Create the 'Previously uploaded files' fieldset and populate it with the
// files from the $previous_files array.
$form['previous_files'] = [
'#type' => 'details',
'#weight' => -2,
'#title' => t('All uploaded files'),
'#open' => TRUE,
'#access' => (bool) $previous_files,
];
/** @var \Drupal\file\FileInterface $file */
foreach ($previous_files as $file) {
[, $version] = _devportal_api_reference_get_data_from_file($file->getFileUri());
$form['previous_files'][] = [
'#theme' => 'file_link',
'#file' => $file,
'#description' => Html::escape("{$file->getFilename()} ({$version})"),
'#cache' => [
'tags' => $file->getCacheTags(),
],
];
}
}
/**
* Adds the mode selector to the api reference node form.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
* @param bool $is_new
* TRUE if the node is new, FALSE otherwise.
*/
function _devportal_api_reference_allow_skip_upload_on_node_form(array &$form, FormStateInterface $form_state, bool $is_new): void {
array_unshift($form['#validate'], 'devportal_api_reference_api_reference_validate');
array_unshift($form['actions']['submit']['#submit'], 'devportal_api_reference_api_reference_submit');
$form['#prefix'] = '<div id="api-reference-node">';
$form['#suffix'] = '</div>';
/** @var \Drupal\node\Entity\Node $node */
$node = $form_state->getFormObject()->getEntity();
/** @var \Drupal\file\Plugin\Field\FieldType\FileItem $file */
$file = $node->get('field_source_file')->get(0);
$has_file = (bool) $file->getValue();
$default_mode = \Drupal::config('devportal_api_reference.settings')->get('manual_mode_default') ? 'manual' : 'upload';
$form['mode_selector'] = [
'#type' => 'radios',
'#title' => t('Mode'),
'#options' => [
'upload' => t('Upload an API reference file'),
'manual' => t('Fill in the values manually'),
],
'#weight' => -128,
'#default_value' => $default_mode,
'#submit' => [
'_devportal_api_reference_node_form_submit',
],
'#ajax' => [
'callback' => '_devportal_api_reference_node_form_callback',
'event' => 'change',
'wrapper' => 'api-reference-node',
],
'#access' => !$has_file,
];
$current_mode = $form_state->getValue('mode_selector') ?: $default_mode;
$manual = $current_mode === 'manual' && !$has_file;
$form['field_source_file']['widget'][0]['#required'] = !$manual && $is_new;
$form['field_source_file']['widget'][0]['#process'][] = '_devportal_api_reference_file_field_process';
$form['title']['#access'] = $manual;
$form['field_description']['#access'] = $manual;
$form['field_version']['#access'] = $manual;
$form['field_source_file']['#access'] = !$manual;
}
/**
* Process callback for the file field on the api reference node form.
*
* @param array $element
* Element array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
* @param array $complete_form
* The complete form.
*
* @return array
* The processed element.
*/
function _devportal_api_reference_file_field_process(array &$element, FormStateInterface $form_state, array &$complete_form): array {
$element['upload_button']['#ajax']['wrapper'] = 'api-reference-node';
$element['upload_button']['#ajax']['callback'] = '_devportal_api_reference_node_form_callback';
return $element;
}
/**
* AJAX callback for the api reference node form.
*
* This callback returns the whole form.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*
* @return array
* Form array.
*/
function _devportal_api_reference_node_form_callback(array &$form, FormStateInterface $form_state): array {
return $form;
}
/**
* AJAX submit callback for the api reference node form.
*
* Sets rebuild on the form.
*
* @param array $form
* Form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*/
function _devportal_api_reference_node_form_submit(array &$form, FormStateInterface $form_state): void {
$form_state->setRebuild(TRUE);
}
/**
* Extracts data from an api reference file.
*
* TODO refactor this.
*
* @param string $uri
* URI to the file.
*
* @return array
* Title, version, description, the full document and the plugin respectively.
*/
function _devportal_api_reference_get_data_from_file(string $uri): ?array {
/** @var \Drupal\devportal_api_reference\ReferenceTypeManager $manager */
$manager = \Drupal::service('plugin.manager.reference');
$ref = $manager->lookupPlugin($uri);
if (!$ref) {
return NULL;
}
$doc = $ref->parse($uri);
return [
$ref->getTitle($doc),
$ref->getVersion($doc),
$ref->getDescription($doc),
$doc,
$ref,
];
}
/**
* Checks whether a given API documentation version already exist or not.
*
* @param \Drupal\node\NodeInterface $node
* The API Reference node entity.
* @param string|null $version
* The API version to check.
*
* @return bool
* Returns TRUE if the given API version already exist. Returns FALSE
* otherwise.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Exception
*/
function devportal_api_reference_check_api_version(NodeInterface $node, ?string $version): bool {
if (\Drupal::config('devportal_api_reference.settings')->get('allow_version_duplication')) {
return FALSE;
}
if ($version === NULL) {
return TRUE;
}
$previous_files = devportal_api_reference_get_previous_files($node->id());
foreach ($previous_files as $file) {
[, $previous_version] = _devportal_api_reference_get_data_from_file($file->getFileUri());
if ($version === $previous_version) {
return TRUE;
}
}
return FALSE;
}
/**
* Returns the files previously added to the node.
*
* @param int|null $nid
* The node ID.
*
* @return array
* An array of objects containing the previously added files.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
function devportal_api_reference_get_previous_files(?int $nid): array {
if ($nid === NULL) {
return [];
}
$previous_files = [];
$query = \Drupal::entityQuery('node');
$revision_ids = $query
->condition('nid', $nid)
->allRevisions()
->execute();
foreach (array_keys($revision_ids) as $vid) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = \Drupal::entityTypeManager()
->getStorage('node')
->loadRevision($vid);
$source = $revision->get('field_source_file')->getValue();
if (!empty($source)) {
$previous_files[] = File::load($source[0]['target_id']);
}
}
// If, for example, the published setting is toggled Drupal will create a new
// revision. In this case Drupal will list that revision in the revisions
// list, thus it will be deletable or revertable through the UI.
// But... we still have the same number of files uploaded. We need to remove
// the duplicate files from the list of previous files.
return devportal_api_reference_get_unique_files($previous_files);
}
/**
* Return objects with unique files from the previous_files array.
*
* @param array $array
* The array of objects to be checked.
*
* @return array
* Returns an array of unique objects.
*/
function devportal_api_reference_get_unique_files(array $array): array {
$duplicate_keys = [];
$tmp = [];
foreach ($array as $key => $val) {
$uri = $val->getFileUri();
if (!in_array($uri, $tmp, TRUE) && file_exists($uri)) {
$tmp[] = $uri;
}
else {
$duplicate_keys[] = $key;
}
}
foreach ($duplicate_keys as $key) {
unset($array[$key]);
}
return $array;
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function devportal_api_reference_node_presave(ContentEntityInterface $entity): void {
if ($entity->bundle() === 'api_reference') {
// Force set revision_translation_affected column in node_field_revision
// table to TRUE, so that all revisions in the database will be viewable on
// the /node/{nid}/revisions page.
$entity->setRevisionTranslationAffected(TRUE);
}
}
/**
* Makes sure that the term exists for a given tag.
*
* @param string $vocabulary
* Vocabulary vid.
* @param null|string $name
* Name of the term. If NULL, then NULL will be returned.
* @param string $description
* Description of the term. Only used when creating the term.
*
* @return \Drupal\taxonomy\Entity\Term|null
* The found / created term. NULL if $name is NULL or if an error occoured.
*/
function devportal_api_reference_ensure_term(string $vocabulary, ?string $name, string $description): ?Term {
if ($name === NULL) {
return NULL;
}
/** @var \Drupal\taxonomy\Entity\Term[] $terms */
$terms = taxonomy_term_load_multiple_by_name($name, $vocabulary);
foreach ($terms as $term) {
if ($term->getName() === $name) {
return $term;
}
}
try {
$term = Term::create([
'vid' => $vocabulary,
'name' => $name,
'description' => $description,
]);
$term->save();
return $term;
}
catch (\Exception $e) {
\Drupal::messenger()->addError(t('Failed to save tags.'));
watchdog_exception('devportal_api_reference', $e);
}
return NULL;
}
