Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/omeka/omeka-s into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewjmckinley committed Jan 31, 2024
2 parents 13c9e64 + 86bed05 commit ca7b326
Show file tree
Hide file tree
Showing 192 changed files with 4,538 additions and 1,747 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.1', '8.2']
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
fail-fast: false
steps:
- name: Checkout
Expand All @@ -36,7 +36,7 @@ jobs:
tools: composer:v1
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Node
uses: actions/setup-node@v3
with:
Expand All @@ -53,31 +53,31 @@ jobs:
path: ${{ env.CI_COMPOSER_CACHE_DIR }}
key: composer-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-

- name: Cache build caches
uses: actions/cache@v3
with:
path: ./build/cache
key: build-cache-${{ github.sha }}
restore-keys: build-cache-

- name: Start MySQL
run: sudo systemctl start mysql.service

- name: Install Node dependencies
run: npm install

- name: Gulp init
run: npx gulp init

- name: Set up database
run: |
mysql -e "create database IF NOT EXISTS omeka_test;" -uroot -proot
sed -i 's/^host.*/host = "localhost"/' application/test/config/database.ini
sed -i 's/^user.*/user = "root"/' application/test/config/database.ini
sed -i 's/^dbname.*/dbname = "omeka_test"/' application/test/config/database.ini
sed -i 's/^password.*/password = "root"/' application/test/config/database.ini
- name: Run tests
run: npx gulp test --continue
env:
Expand Down
2 changes: 1 addition & 1 deletion .htaccess.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ RewriteEngine On
# The following rule tells Apache that if the requested filename
# exists, simply serve it.
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule !\.(php[0-9]?|phtml|phps)$ - [NC,C]
RewriteRule !\.(php[0-9]?|phtml|phps|phar|hphp)$ - [NC,C]
RewriteRule !(?:^|/)\.(?!well-known(?:/.*)?$) - [C]
RewriteRule .* - [L]

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ See the [user manual](https://omeka.org/s/docs/user-manual) for more information
### Requirements
* Linux
* [Apache](https://www.apache.org/) (with [AllowOverride](https://httpd.apache.org/docs/2.4/mod/core.html#allowoverride) set to "All" and [mod_rewrite](http://httpd.apache.org/docs/current/mod/mod_rewrite.html) enabled)
* [MySQL](https://www.mysql.com/) 5.6.4+ (or [MariaDB](https://mariadb.org/) 10.0.5+)
* [MySQL](https://www.mysql.com/) 5.7.9+ (or [MariaDB](https://mariadb.org/) 10.2.6+)
* [PHP](https://www.php.net/) 7.4+ (latest stable version preferred, with [PDO](http://php.net/manual/en/intro.pdo.php), [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php), and [xml](http://php.net/manual/en/intro.xml.php) extensions installed)

### Generating thumbnails
Expand Down
209 changes: 180 additions & 29 deletions application/Module.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<?php
namespace Omeka;

use EasyRdf\Graph;
use Omeka\Api\Adapter\FulltextSearchableInterface;
use Omeka\Api\Representation\AbstractResourceEntityRepresentation;
use Omeka\Api\Representation\RepresentationInterface;
use Omeka\Entity\Item;
use Omeka\Entity\Media;
use Omeka\Module\AbstractModule;
use Laminas\EventManager\Event as ZendEvent;
use Laminas\EventManager\SharedEventManagerInterface;
use Laminas\Form\Element;
use Laminas\Json\Json;
use Laminas\View\Renderer\PhpRenderer;

/**
Expand All @@ -18,7 +22,7 @@ class Module extends AbstractModule
/**
* This Omeka version.
*/
const VERSION = '4.1.0-alpha';
const VERSION = '4.1.0-rc';

/**
* The vocabulary IRI used to define Omeka application data.
Expand Down Expand Up @@ -121,20 +125,14 @@ public function attachListeners(SharedEventManagerInterface $sharedEventManager)

$sharedEventManager->attach(
'Omeka\Entity\Media',
'entity.persist.post',
[$this, 'saveFulltextOnMediaSave']
);

$sharedEventManager->attach(
'Omeka\Entity\Media',
'entity.update.post',
[$this, 'saveFulltextOnMediaSave']
'entity.remove.pre',
[$this, 'deleteFulltextMedia']
);

$sharedEventManager->attach(
'Omeka\Api\Adapter\SitePageAdapter',
'api.delete.pre',
[$this, 'deleteFulltextPre']
[$this, 'deleteFulltextPreSitePage']
);

$sharedEventManager->attach(
Expand Down Expand Up @@ -178,6 +176,44 @@ public function attachListeners(SharedEventManagerInterface $sharedEventManager)
[$this, 'noindexItemSet']
);

// Add favicon to layouts.
$sharedEventManager->attach(
'*',
'view.layout',
function (ZendEvent $event) {
$view = $event->getTarget();
// Get the favicon asset ID.
if ($view->status()->isSiteRequest()) {
$faviconAssetId = $view->siteSetting('favicon');
if (!is_numeric($faviconAssetId)) {
$faviconAssetId = $view->setting('favicon');
}
} else {
$faviconAssetId = $view->setting('favicon');
}
// Get the favicon href.
if (is_numeric($faviconAssetId)) {
$faviconAsset = $view->api()->searchOne('assets', ['id' => $faviconAssetId])->getContent();
$href = $faviconAsset ? $faviconAsset->assetUrl() : null;
} else {
$href = null; // Passing null clears the favicon.
}
$view->headLink(['rel' => 'icon', 'href' => $href], 'PREPEND');
}
);

$sharedEventManager->attach(
'*',
'api.output.serialize',
[$this, 'serializeApiOutputJsonLd']
);

$sharedEventManager->attach(
'*',
'api.output.serialize',
[$this, 'serializeApiOutputRdf']
);

$sharedEventManager->attach(
'*',
'sql_filter.resource_visibility',
Expand Down Expand Up @@ -564,34 +600,42 @@ public function saveFulltext(ZendEvent $event)
{
$adapter = $event->getTarget();
$entity = $event->getParam('response')->getContent();
$fulltextSearch = $this->getServiceLocator()->get('Omeka\FulltextSearch');
$fulltextSearch->save($entity, $adapter);

// Item create needs special handling. We must save media fulltext here
// because media is created via cascade persist (during item create/update),
// which is invisible to normal API events.
if ($entity instanceof Item) {
$mediaAdapter = $adapter->getAdapter('media');
foreach ($entity->getMedia() as $mediaEntity) {
$fulltextSearch->save($mediaEntity, $mediaAdapter);
}
}
// Item media needs special handling. We must update the item's fulltext
// to append updated media data.
if ($entity instanceof Media) {
// Media get special handling during entity.persist.post and
// entity.update.post in self::saveFulltextOnMediaSave(). There's no
// need to process them here.
return;
$itemEntity = $entity->getItem();
$itemAdapter = $adapter->getAdapter('items');
$fulltextSearch->save($itemEntity, $itemAdapter);
}
$fulltext = $this->getServiceLocator()->get('Omeka\FulltextSearch');
$fulltext->save($entity, $adapter);
}

/**
* Save fulltext on media save.
* Delete the fulltext of a media.
*
* This method does two things. First, it updates the parent item's fulltext
* to contain any new text introduced by this media. Second it ensures that
* the fulltext of newly created media is saved. Otherwise, media created
* in the item context (via cascade persist) will not have fulltext.
* We must delete media fulltext here because media may be deleted via cascade
* remove (during item update), which is invisible to normal API events.
*
* @param ZendEvent $event
*/
public function saveFulltextOnMediaSave(ZendEvent $event)
public function deleteFulltextMedia(ZendEvent $event)
{
$fulltextSearch = $this->getServiceLocator()->get('Omeka\FulltextSearch');
$adapterManager = $this->getServiceLocator()->get('Omeka\ApiAdapterManager');
$mediaEntity = $event->getTarget();
$itemEntity = $mediaEntity->getItem();
$fulltextSearch->save($mediaEntity, $adapterManager->get('media'));
$fulltextSearch->save($itemEntity, $adapterManager->get('items'));
$mediaAdapter = $adapterManager->get('media');
$fulltextSearch->delete($mediaEntity->getId(), $mediaAdapter);
}

/**
Expand All @@ -603,7 +647,7 @@ public function saveFulltextOnMediaSave(ZendEvent $event)
*
* @param ZendEvent $event
*/
public function deleteFulltextPre(ZendEvent $event)
public function deleteFulltextPreSitePage(ZendEvent $event)
{
$request = $event->getParam('request');
$em = $this->getServiceLocator()->get('Omeka\EntityManager');
Expand All @@ -624,10 +668,24 @@ public function deleteFulltextPre(ZendEvent $event)
*/
public function deleteFulltext(ZendEvent $event)
{
$fulltext = $this->getServiceLocator()->get('Omeka\FulltextSearch');
$adapter = $event->getTarget();
$entity = $event->getParam('response')->getContent();
$request = $event->getParam('request');
$fulltext->delete(
// Note that the resource may not have an ID after being deleted.
$fulltextSearch = $this->getServiceLocator()->get('Omeka\FulltextSearch');

// Media delete needs special handling. We must update the item's fulltext
// to remove the appended media data. We return here because deleting media
// fulltext is handled by self::deleteFulltextMedia().
if ($entity instanceof Media) {
$itemEntity = $entity->getItem();
$itemAdapter = $adapter->getAdapter('items');
$fulltextSearch->save($itemEntity, $itemAdapter);
return;
}

// Note that the resource may not have an ID after being deleted. This
// is why we must use $request->getId() rather than $entity->getId().
$fulltextSearch->delete(
$request->getOption('deleted_entity_id') ?? $request->getId(),
$event->getTarget()
);
Expand Down Expand Up @@ -743,6 +801,99 @@ public function noindexItemSet(ZendEvent $event)
$this->noindexResourceShow($view, $view->itemSet);
}

/**
* Serialize the API output to JSON-LD.
*/
public function serializeApiOutputJsonLd(ZendEvent $event)
{
$renderer = $event->getTarget();
$model = $event->getParam('model');
$format = $event->getParam('format');
$payload = $event->getParam('payload');
$output = $event->getParam('output');

if ('jsonld' !== $format) {
return;
}

$eventManager = $this->getServiceLocator()->get('EventManager');

if ($payload instanceof RepresentationInterface) {
$args = $eventManager->prepareArgs(['jsonLd' => $output]);
$eventManager->trigger('rep.resource.json_output', $payload, $args);
$output = $args['jsonLd'];
}

if (null !== $model->getOption('pretty_print')) {
// Pretty print the JSON.
$output = Json::prettyPrint($output);
}

$jsonpCallback = (string) $model->getOption('callback');
if (!empty($jsonpCallback)) {
// Wrap the JSON in a JSONP callback. Normally this would be done
// via `$this->setJsonpCallback()` but we don't want to pass the
// wrapped string to `rep.resource.json_output` handlers.
$output = sprintf('%s(%s);', $jsonpCallback, $output);
$renderer->setHasJsonpCallback(true);
}

$event->setParam('output', $output);
}

/**
* Serialize the API output to RDF formats (rdfxml, n3, turtle, ntriples).
*/
public function serializeApiOutputRdf(ZendEvent $event)
{
$renderer = $event->getTarget();
$model = $event->getParam('model');
$format = $event->getParam('format');
$payload = $event->getParam('payload');
$output = $event->getParam('output');

if (!in_array($format, ['rdfxml', 'n3', 'turtle', 'ntriples'])) {
return;
}

$eventManager = $this->getServiceLocator()->get('EventManager');

$serializeRdf = function ($jsonLd) use ($format) {
$graph = new Graph;
$graph->parse(Json::encode($jsonLd), 'jsonld');
return $graph->serialise($format);
};

$getJsonLdWithContext = function (RepresentationInterface $representation) use ($eventManager) {
// Add the @context by encoding the output as JSON, then decoding to an array.
static $context;
if (!$context) {
// Get the JSON-LD @context
$args = $eventManager->prepareArgs(['context' => []]);
$eventManager->trigger('api.context', null, $args);
$context = $args['context'];
}
$jsonLd = Json::decode(Json::encode($representation), true);
$jsonLd['@context'] = $context;
return $jsonLd;
};

// Render a single representation (get).
if ($payload instanceof RepresentationInterface) {
$jsonLd = $getJsonLdWithContext($payload);
$output = $serializeRdf($jsonLd);
// Render multiple representations (getList);
} elseif (is_array($payload) && array_filter($payload, fn ($object) => ($object instanceof RepresentationInterface))) {
$jsonLd = [];
foreach ($payload as $representation) {
$jsonLd[] = $getJsonLdWithContext($representation);
}
$output = $serializeRdf($jsonLd);
}

$event->setParam('output', $output);
}

/**
* Add a robots "noindex" metatag to the current view if the resource
* being viewed does not belong to the current site.
Expand Down
Loading

0 comments on commit ca7b326

Please sign in to comment.