l10n_server-2.x-dev/l10n_packager/src/L10nPackager.php

l10n_packager/src/L10nPackager.php
<?php

declare(strict_types=1);

namespace Drupal\l10n_packager;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\FileSecurity\FileSecurity;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Timer;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\l10n_packager\Entity\L10nPackagerFileInterface;
use Drupal\l10n_packager\Entity\L10nPackagerRelease;
use Drupal\l10n_packager\Entity\L10nPackagerReleaseInterface;
use Drupal\l10n_server\Entity\L10nServerProjectInterface;
use Drupal\l10n_server\Entity\L10nServerReleaseInterface;

/**
 * Service description.
 */
class L10nPackager {

  /**
   * Cache expiration time for the l10n_packager cache.
   */
  private const CACHE_EXPIRATION = 3600;

  /**
   * Release packager status: do not repackage anymore.
   */
  const DISABLED = 0;

  /**
   * Release packager status: keep repackaging.
   */
  const ACTIVE = 1;

  /**
   * Release packager status: error.
   */
  const ERROR = 2;

  /**
   * Default path structure for generated files.
   */
  const FILEPATH = '%core/%project/%project-%release.%language.po';

  /**
   * Packager API version.
   */
  const API_VERSION = '1.1';

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $database;

  /**
   * Packager settings.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $packagerSettings;

  /**
   * Community settings.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $communitySettings;

  /**
   * Entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected FileSystemInterface $fileSystem;

  /**
   * Logger service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Time (datetime.time) service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * Date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * File URL generator service.
   *
   * @var \Drupal\Core\File\FileUrlGeneratorInterface
   */
  protected $fileUrlGenerator;

  /**
   * L10n exporter service.
   *
   * @var \Drupal\l10n_packager\L10nExporter
   */
  protected $exporter;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandler
   */
  protected $moduleHandler;

  /**
   * Language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructs a L10nPackager object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system service.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   The logger service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   Time (datetime.time) service.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   Date formatter service.
   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
   *   File URL generator service.
   * @param \Drupal\l10n_packager\L10nExporter $exporter
   *   L10n exporter service.
   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
   *   Module handler service.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   Language manager service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
   *   The cache backend.
   */
  public function __construct(
    Connection $database,
    ConfigFactoryInterface $config_factory,
    EntityTypeManagerInterface $entity_type_manager,
    FileSystemInterface $file_system,
    LoggerChannelInterface $logger,
    TimeInterface $time,
    DateFormatterInterface $date_formatter,
    FileUrlGeneratorInterface $file_url_generator,
    L10nExporter $exporter,
    ModuleHandler $module_handler,
    LanguageManagerInterface $language_manager,
    private readonly CacheBackendInterface $cacheBackend,
  ) {
    $this->database = $database;
    $this->packagerSettings = $config_factory->get('l10n_packager.settings');
    $this->communitySettings = $config_factory->get('l10n_community.settings');
    $this->entityTypeManager = $entity_type_manager;
    $this->fileSystem = $file_system;
    $this->logger = $logger;
    $this->time = $time;
    $this->dateFormatter = $date_formatter;
    $this->fileUrlGenerator = $file_url_generator;
    $this->exporter = $exporter;
    $this->moduleHandler = $module_handler;
    $this->languageManager = $language_manager;
  }

  /**
   * Get files for a release, indexed by language.
   *
   * @param int $rid
   *   A L10nServerRelease id.
   *
   * @return \Drupal\l10n_packager\Entity\L10nPackagerFileInterface[]
   *   An associative array, or an empty array if there is no result set.
   */
  public function getFiles($rid): array {
    $files = $this->entityTypeManager
      ->getStorage('l10n_packager_file')
      ->loadByProperties(['rid' => $rid]);

    $files_per_language = [];
    foreach ($files as $file) {
      $files_per_language[$file->language->value] = $file;
    }

    return $files_per_language;
  }

  /**
   * Get timestamp of the last updated string for a release, for each language.
   *
   * @param int $rid
   *   A L10nServerRelease id.
   *
   * @return array
   *   A keyed query result array.
   */
  public function translationLastUpdated($rid) {
    $query = $this->database
      ->select('l10n_server_translation', 't');
    $query
      ->innerJoin('l10n_server_line', 'l', 't.sid = l.sid');
    $query
      ->addExpression('MAX(t.changed)', 'latest_time');
    $query
      ->fields('t', ['language']);
    $query
      ->condition('t.status', 1)
      ->condition('t.suggestion', 0)
      ->condition('l.rid', $rid);
    $query
      ->groupBy('t.language');
    return $query->execute()
      ->fetchAllKeyed();
  }

  /**
   * Get release name.
   *
   * @param int $rid
   *   A l10n_server_release id.
   *
   * @return string
   *   A normalized release name.
   */
  public function releaseName($rid) {
    if ($release = $this->entityTypeManager->getStorage('l10n_server_release')->load($rid)) {
      return $release->getProject()->getUri() . '-' . $release->getTitle();
    }
    else {
      return '';
    }
  }

  /**
   * Build target filepath from release object based on the set pattern.
   *
   * @param \Drupal\l10n_server\Entity\L10nServerReleaseInterface $release
   *   A l10n_server_release entity.
   * @param \Drupal\Core\Language\LanguageInterface|null $language
   *   A language object.
   * @param string|null $pattern
   *   A pattern string.
   *
   * @return string
   *   The filepath string.
   */
  public function getFilepath(L10nServerReleaseInterface $release, ?LanguageInterface $language = NULL, ?string $pattern = NULL) {
    $project = $release->getProject();
    $replace = [
      '%project' => $project->getUri(),
      '%release' => $release->getVersion(),
      '%core' => $release->getCoreFromVersion(),
      '%version' => $release->getVersion(),
      '%branch' => $release->getBranchFromVersion(),
      '%extra' => !empty($extra) ? '-' . $extra : '',
      '%language' => isset($language) ? $language->getId() : '',
    ];
    if (!isset($pattern)) {
      $pattern = $this->packagerSettings->get('filepath');
    }
    return strtr($pattern, $replace);
  }

  /**
   * Create a symlink to the latest version of this locale for the project.
   *
   * The symlink name has the pattern [project]-[branch].[langcode].po and will
   * be placed is the same directory as the translation file.
   *
   * @param string $uri
   *   A stream wrapper URI or a filepath.
   * @param \Drupal\l10n_packager\Entity\L10nPackagerReleaseInterface $release
   *   An object containing the file's release information.
   * @param \Drupal\Core\Language\LanguageInterface $language
   *   A language object.
   *
   * @return bool
   *   Returns TRUE if a symlink was created.
   */
  public function createLatestSymlink(string $uri, L10nPackagerReleaseInterface $release, LanguageInterface $language): bool {
    // If there is a minor version number, remove it. “Branch” is only
    // '{major}.x' or '{compatibility}-{major}.x'. So a new dev branch can fall
    // back to the latest translation for the major branch. For example, 9.1.x,
    // when there are no tagged 9.1.* releases, can get the 9.0.0-beta1
    // translations.
    $abbreviated_release = $this->entityTypeManager
      ->getStorage('l10n_server_release')
      ->load($release->id());

    $target = $this->fileSystem->realpath($uri);
    $latest_file = dirname($target) . '/' . $this->getFilepath($abbreviated_release, $language, '%project-%branch.%language.po');

    if (file_exists($latest_file)) {
      unlink($latest_file);
      $latest_file_object = new \stdClass();
      $latest_file_object->uri = $this->packagerSettings->get('directory') . '/' . $this->getFilepath($abbreviated_release, $language, '%core/%project/%project-%branch.%language.po');
      // Allow modules to react to the symlink, such as purge a CDN.
      $this->moduleHandler->invokeAll('l10n_packager_done', [$latest_file_object]);
    }

    return symlink(basename($target), $latest_file);
  }

  /**
   * Generate a new file for a given release or update an existing one.
   *
   * @param \Drupal\l10n_packager\Entity\L10nPackagerReleaseInterface $packager_release
   *   Release object with uri and rid properties.
   * @param \Drupal\Core\Language\LanguageInterface $language
   *   Language object.
   * @param \Drupal\l10n_packager\Entity\L10nPackagerFileInterface|null $packager_file
   *   Release file object to be updated.
   * @param int|null $timestamp
   *   Timestamp to mark the files, for it to be consistent across tables.
   *
   * @return \Drupal\file\Entity\File|false
   *   Drupal file object or FALSE on error.
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function package(L10nPackagerReleaseInterface $packager_release, LanguageInterface $language, ?L10nPackagerFileInterface $packager_file = NULL, ?int $timestamp = NULL): bool|File {
    $release = $this->entityTypeManager
      ->getStorage('l10n_server_release')
      ->load($packager_release->id());

    $timestamp = $timestamp ?: $this->time->getRequestTime();
    $variables = [
      '%release' => $this->releaseName($packager_release->id()),
      '%language' => $language->getId(),
    ];

    if (!$packager_file) {
      $packager_file = $this->entityTypeManager
        ->getStorage('l10n_packager_file')
        ->create([
          'rid' => $packager_release->id(),
          'language' => $language->getId(),
        ]);
    }

    // Generate PO file. Always export in compact form.
    $export_result = $this->exporter->export($release->getProject()->getUri(), $release->id(), $language, FALSE, TRUE);

    if (!empty($export_result) && is_array($export_result)) {

      // If we got an array back from the export build, tear that into pieces.
      [$mime_type, $export_name, $serve_name, $sid_count] = $export_result;

      // Get the destination file path.
      $file_path = $this->getFilepath($release, $language);
      // Build the full path and make sure the directory exits.
      $file_path = $this->createPath($file_path);

      // Now build the Drupal file object or update the old one.
      if (!$packager_file->fid->isEmpty()) {
        $file = $packager_file->fid->entity;
        $this->fileSystem->delete($file->getFileUri());
      }
      else {
        $file = $this->entityTypeManager
          ->getStorage('file')
          ->create(['created' => $timestamp]);
      }

      // Check / update / create all file data.
      $file
        ->set('status', FileInterface::STATUS_PERMANENT)
        ->set('changed', $timestamp)
        ->set('filename', basename($file_path))
        ->set('filemime', $mime_type)
        ->set('uri', $file_path);
      $this->fileSystem->move($export_name, $file->getFileUri(), FileSystemInterface::EXISTS_REPLACE);
      $file
        ->set('filesize', filesize($this->fileSystem->realpath($file->getFileUri())));
      $file->save();

      // Create actual symlink to the latest release.
      $this->createLatestSymlink($file_path, $packager_release, $language);

      $packager_file
        ->set('rid', $packager_release->id())
        ->set('language', $language->getId())
        ->set('fid', $file->id())
        ->set('sid_count', $sid_count)
        ->set('checked', $timestamp)
        ->save();
      $this->moduleHandler->invokeAll('l10n_packager_done', [$file]);
      return $file;
    }
    else {
      $this->logger->error('Failed packaging release %release for language %language.', $variables);
      return FALSE;
    }
  }

  /**
   * Check releases that need repackaging.
   *
   * @param string $project
   *   The project to scan.
   *
   * @return array
   *   Number of projets checked, number of files, elapsed time.
   */
  public function checkUpdates(string $project = '') : array {
    $count_check = $count_files = $time = 0;
    $updates = [];

    if ($interval = $this->packagerSettings->get('update')) {
      Timer::start('l10n_packager');
      $timestamp = $this->time->getRequestTime() - $interval;
      $file_limit = $this->packagerSettings->get('file_limit');
      $count_files = $count_check = 0;

      // Go for it: check releases for repackaging. We need project_uri for
      // later.
      $query = $this->database->select('l10n_server_release', 'r');
      $query->innerJoin('l10n_server_project', 'p', 'r.pid = p.pid');
      $query->leftJoin('l10n_packager_release', 'pr', 'pr.rid = r.rid');
      $query->addField('r', 'rid');
      $query->addField('pr', 'status');
      if ($project) {
        $query->condition('p.uri', $project);
      }
      else {
        $query->condition($query->orConditionGroup()
          ->isNull('pr.status')
          ->condition($query->andConditionGroup()
            ->condition('pr.status', L10nPackagerRelease::ACTIVE)
            ->condition($query->orConditionGroup()
              ->condition('pr.checked', $timestamp, '<')
              ->condition('pr.changed', $timestamp, '<')
            )
          )
        );
      }
      $query->range(0, $this->packagerSettings->get('release_limit'));
      $query->orderBy('pr.checked');
      $results = $query->execute();

      $storage = $this->entityTypeManager->getStorage('l10n_packager_release');
      while ((!$file_limit || $file_limit > $count_files) && ($result = $results->fetchObject())) {
        $release = is_null($result->status) ? $storage->create(['rid' => $result->rid]) : $storage->load($result->rid);
        $updates = $this->check($release, FALSE, $file_limit - $count_files, NULL, TRUE);
        $count_files += count($updates);
        $count_check++;
      }

      $time = Timer::stop('l10n_packager')['time'];

      $this->logger->notice('@ms ms for %checked releases/%repack files.', [
        '%checked' => $count_check,
        '%repack' => $count_files,
        '@ms' => $time,
      ]);
    }

    return [$count_check, $count_files, $time];
  }

  /**
   * Check release translations and repackage if needed.
   *
   * For each release we store packaging data in {l10n_packager_release}
   * - 'checked' is the last time all languages for this release were checked.
   * - 'updated' is the last time a file was updated for this release.
   *
   * We don't update the 'checked' field until we've checked all the languages
   * for this release, so we can keep track of releases and files and package a
   * few languages on every cron.
   *
   * @param \Drupal\l10n_packager\Entity\L10nPackagerReleaseInterface $packager_release
   *   Release object.
   * @param bool $force
   *   Force repackage even when strings not updated.
   * @param int $limit
   *   Maximum number of files to create.
   * @param \Drupal\Core\Language\LanguageInterface|null $language
   *   Optional language object to check only this one.
   * @param bool $cron
   *   In a cron run, a release may be packaged partially, for some languages.
   *
   * @return array
   *   An associative array of File entities indexed by langcode.
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  public function check(L10nPackagerReleaseInterface $packager_release, bool $force = FALSE, int $limit = 0, ?LanguageInterface $language = NULL, bool $cron = FALSE) {
    $check_languages = isset($language) ? [$language->getId() => $language] : $this->languageManager->getLanguages();
    $updated = [];
    // We get update time before creating files so the release checked time
    // is <= file timestamp.
    $timestamp = $this->time->getRequestTime();

    $files = $this->getFiles($packager_release->id());
    $last_updated = $this->translationLastUpdated($packager_release->id());

    // Get only the languages we have translations for, that need updating.
    $languages = [];
    foreach ($check_languages as $langcode => $language) {
      if (!empty($last_updated[$langcode]) && ($force || empty($files[$langcode]) || ($last_updated[$langcode] > $files[$langcode]->getCheckedTime()))) {
        $languages[$langcode] = $language;
      }
    }

    // For this special case we check we didn't stop before in the middle of a
    // release. Otherwise it could stick on a release forever when forcing.
    if ($force && $cron && $packager_release->getCheckedTime() < $packager_release->getChangedTime()) {
      foreach ($files as $lang => $file) {
        if (!$file->checked->isEmpty() && ($file->isChecked() > $packager_release->getCheckedTime())) {
          unset($languages[$lang]);
        }
      }
    }

    // Repackage this release for the remaining language list.
    while ((!$limit || $limit > count($updated)) && ($language = array_shift($languages))) {
      $langcode = $language->getId();
      // Warning: this may upload release data with or without file.
      $existing = !empty($files[$langcode]) ? $files[$langcode] : NULL;
      $updated[$langcode] = $this->package($packager_release, $language, $existing, $timestamp);
    }

    // Update the release data.
    if (!count($languages)) {
      // We only mark the release checked if there are no languages left.
      $packager_release->set('checked', $timestamp);
    }
    if ($updated) {
      $packager_release->set('changed', $timestamp);
    }

    $packager_release->save();
    return $updated;
  }

  /**
   * Ensure that directories on the $path exist in our packager directory.
   *
   * @param string $path
   *   A path string.
   *
   * @return string
   *   A path string.
   */
  public function createPath(string $path) {
    $directory = dirname($path);
    $basepath = $currentpath = $this->packagerSettings->get('directory');
    $finalpath = $basepath . '/' . $directory;
    $parts = explode('/', $directory);

    $htaccess_path = $this->fileSystem->realpath($basepath) . '/.htaccess';
    if (!is_dir($finalpath)) {
      $this->fileSystem->prepareDirectory($finalpath, FileSystemInterface::CREATE_DIRECTORY);
    }
    if (!file_exists($htaccess_path)) {
      $htaccess_lines = "\n\n<FilesMatch \"\.(po)$\">\n\tForceType \"text/plain; charset=utf-8\"\n\tAllow from ALL\n</FilesMatch>\n";
      FileSecurity::writeHtaccess($basepath, FALSE);
      $this->fileSystem->chmod($htaccess_path, 0744);
      file_put_contents($htaccess_path, $htaccess_lines, FILE_APPEND);
      $this->fileSystem->chmod($htaccess_path, 0444);
    }

    return $basepath . '/' . $path;
  }

  /**
   * Load and return the highlighted project if set and found.
   */
  public function getHighlightedProject() {
    if ($highlight_project = $this->communitySettings->get('highlighted_project')) {
      $storage = $this->entityTypeManager->getStorage('l10n_server_project');
      $entity_ids = $storage
        ->getQuery()
        ->accessCheck(TRUE)
        ->condition('title', $highlight_project)
        ->execute();
      $project = $storage->load(reset($entity_ids));
      if ($project) {
        return $project;
      }
    }
    return NULL;
  }

  /**
   * Build download table rows for the language view pages.
   *
   * @param \Drupal\l10n_server\Entity\L10nServerProjectInterface $project
   *   A l10n_server_project entity.
   * @param \Drupal\Core\Language\LanguageInterface $language
   *   A language object.
   * @param bool $summary
   *   Whether to do a summary.
   * @param array $headers
   *   An array of table headers.
   *
   * @return array|array[]
   *   A renderable array.
   */
  public function getProjectDownloads(L10nServerProjectInterface $project, LanguageInterface $language, bool $summary = FALSE, array $headers = []): array {
    $summary = ($summary) ? 'summary' : 'detail';
    $cid = 'l10n_packager:' . $project->id() . ':' . $language->getId() . ':' . $summary;
    if ($cache = $this->cacheBackend->get($cid)) {
      return $cache->data;
    }

    $files = [];
    $branches = [];
    $result = $this->database->query("SELECT r.rid, r.version, pr.checked as release_checked, lf.checked, f.filename, f.filesize, f.uri, f.created as timestamp FROM {l10n_server_release} r INNER JOIN {l10n_packager_release} pr ON r.rid = pr.rid INNER JOIN {l10n_packager_file} lf ON r.rid = lf.rid INNER JOIN {file_managed} f ON lf.fid = f.fid WHERE r.pid = :pid AND lf.language = :language;", [
      ':pid' => $project->id(),
      ':language' => $language->getId(),
    ]);
    foreach ($result as $item) {
      // phpcs:ignore
      // @todo: Fix bad, bad harcoding of Drupal versioning schemes.
      $branch = preg_replace(($project->getUri() == 'drupal' ? '!^(\d\.)(.+)$!' : '!^(\d\.x-\d\.)(.+)$!'), '\1', $item->version) . 'x';
      $branches[$branch] = TRUE;
      $files[$branch][$item->rid] = $item;
    }

    if (empty($branches)) {
      return [
        [
          $project->label(),
          [
            'data' => t('Not available for download yet.'),
            'colspan' => 4,
          ],
        ],
      ];
    }

    ksort($branches);

    $rows = [];
    $summary_list = [];
    $uri_class = str_replace('_', '-', $project->getUri());
    foreach (array_keys($branches) as $branch) {
      // Grab the latest item and compute its up-to-date stats.
      krsort($files[$branch]);
      $latest_item = array_shift($files[$branch]);
      $up_to_date = max($latest_item->checked, $latest_item->release_checked);

      $rows[] = [
        'class' => ['l10n-packager-detail l10n-packager-detail-' . $uri_class],
        'data' => [
          [
            'data' => new FormattableMarkup('<a href=":link">@name</a>', [
              ':link' => Url::fromUri('internal:/translate/projects/' . $project->id())->toString(),
              '@name' => $project->label(),
            ]),
          ],
          $latest_item->version,
          [
            'data' => new FormattableMarkup('<a href=":link">@name <span class="filesize">(@filesize)</span></a>', [
              ':link' => $this->getDownloadUrl($project, $branch, $latest_item),
              '@name' => t('Download'),
              '@filesize' => format_size($latest_item->filesize),
            ]),
          ],
          $this->dateFormatter->format($latest_item->timestamp, 'custom', 'Y-M-d'),
          $this->dateFormatter->format($up_to_date, 'custom', 'Y-M-d'),
        ],
      ];
      $summary_list[] = $this->getDownloadUrl($project, $branch, $latest_item);
    }

    if (count($summary_list) && $summary) {
      $summary_row = [
        'class' => ['l10n-packager-summary'],
        'id' => 'l10n-packager-summary-' . $uri_class,
        'data' => [
          [
            'data' => [
              '#type' => 'link',
              '#title' => '&#9658; ' . $project->label(),
              '#href' => 'translate/projects/' . $project->getUri(),
              '#options' => [
                'attributes' => ['class' => ['expand']],
                'html' => TRUE,
              ],
            ],
          ],
          [
            'data' => implode(', ', $summary_list),
            'colspan' => 4,
          ],
        ],
      ];
      $headers = [
        'class' => ['l10n-packager-detail l10n-packager-detail-' . $uri_class],
        'data' => $headers,
      ];
      array_unshift($rows, $headers);
      array_unshift($rows, $summary_row);
    }

    $this->cacheBackend->set($cid, $rows, $this->time->getRequestTime() + self::CACHE_EXPIRATION);

    return $rows;
  }

  /**
   * Generate a download URL for a PO file.
   *
   * @param \Drupal\l10n_server\Entity\L10nServerProjectInterface $project
   *   A l10n_server_project entity.
   * @param string $branch
   *   A branch string.
   * @param object $file
   *   A file object.
   *
   * @return string
   *   A URL string for download.
   */
  public function getDownloadUrl(L10nServerProjectInterface $project, string $branch, $file) {
    $download_url = '';
    $update_url = $this->packagerSettings->get('update_url');
    if ($update_url) {
      if (!empty($file->rid)) {
        $release = $this->entityTypeManager->getStorage('l10n_server_release')->load($file->rid);
        if ($release) {
          $download_url = $update_url . '/' . $release->getCoreFromVersion() . '/' . $project->getUri() . '/' . $file->filename;
        }
      }
    }
    else {
      $download_url = $this->fileUrlGenerator->generateAbsoluteString($file->uri);
    }

    return $download_url;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc