Merge branch 'master' into PHRAS-2184-list-manager-email-domain-filter
@@ -15,7 +15,7 @@ jobs:
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
docker:
|
||||
- image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37
|
||||
command: /sbin/init
|
||||
- image: circleci/rabbitmq:3.7.7
|
||||
steps:
|
||||
- checkout
|
||||
- run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
|
||||
@@ -24,10 +24,7 @@ jobs:
|
||||
command: nvm install v10.12.0 && nvm alias default v10.12.0
|
||||
- run:
|
||||
working_directory: ~/alchemy-fr/Phraseanet
|
||||
command: 'sudo service memcached status || sudo service memcached start; sudo
|
||||
redis-cli ping >/dev/null 2>&1 || sudo service redis-server start; sudo
|
||||
service mysql status || sudo service mysql start; sudo service rabbitmq-server
|
||||
status || sudo service rabbitmq-server start; '
|
||||
command: 'sudo service mysql status || sudo service mysql start;'
|
||||
# Dependencies
|
||||
# This would typically go in either a build or a build-and-test job when using workflows
|
||||
# Restore the dependency cache
|
||||
@@ -124,7 +121,7 @@ workflows:
|
||||
context: "AWS London"
|
||||
create-repo: true
|
||||
dockerfile: Dockerfile
|
||||
extra-build-args: "--target phraseanet"
|
||||
extra-build-args: "--target phraseanet-fpm"
|
||||
region: AWS_DEFAULT_REGION
|
||||
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet"
|
||||
tag: "alpha-0.1"
|
||||
@@ -139,3 +136,14 @@ workflows:
|
||||
region: AWS_DEFAULT_REGION
|
||||
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet-nginx"
|
||||
tag: "alpha-0.1"
|
||||
- aws-ecr/build_and_push_image:
|
||||
account-url: AWS_ACCOUNT_URL
|
||||
aws-access-key-id: AWS_ACCESS_KEY_ID
|
||||
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
|
||||
context: "AWS London"
|
||||
create-repo: true
|
||||
dockerfile: Dockerfile
|
||||
extra-build-args: "--target phraseanet-worker"
|
||||
region: AWS_DEFAULT_REGION
|
||||
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet"
|
||||
tag: "alpha-0.1"
|
||||
|
140
CHANGELOG.md
@@ -1,14 +1,140 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 4.0.9
|
||||
|
||||
## 4.0.0 (xxxx-xx-xx)
|
||||
### Adds
|
||||
|
||||
- Convert Orders custom adapter to Doctrine entity.
|
||||
- Convert Feeds custom adapter to Doctrine entity.
|
||||
- Convert Users custom adapter to Doctrine entity.
|
||||
- Convert Ftp Export custom adapter to Doctrine entity.
|
||||
- Convert Ftp Export custom adapter to Doctrine entity.
|
||||
- Session management is now part of Phraseanet configuration.
|
||||
- PHRAS-2535 - Back / Front - Unsubscription: It's now possible to request a validation by email to delete a Phraseanet user account.
|
||||
- PHRAS-2480 - Back / Front - It's now possible to add a user model as order manager on a collection:All users with this model applied can manage orders on this collection. This features fixes an issue when users is provided by SAML and the orders manager is lost when user logs in.
|
||||
- PHRAS-2474 - Back / front. - Searched terms are now found even if the searched terms are split in Business Field and regular Field.
|
||||
- PHRAS-2462 - Front - Share media on LinkedIn as you can do on Facebook, Twitter.
|
||||
- PHRAS-2417 - Front - Skin: grey and white, graphic enhancements.
|
||||
- PHRAS-2067 - Front - Introducing thumbnail & preview generic images for Fonts
|
||||
|
||||
### Fixes
|
||||
|
||||
* PHRAS-2491 - Front - Click on facets title (expand/collapse) launched a bad query, due to jquery error.
|
||||
* PHRAS-2510 - Front - Facets values appear Truncated after 15th character.
|
||||
* PHRAS-2153 - Front - No user search possible with the field "Company" and field "Country".
|
||||
* PHRAS-2154 - Front - Bug on Chrome only - selected 1 document instead of all for the feedback.
|
||||
* PHRAS-2538 - Back - Some MP4 files were not correctly detected by Phraseanet.
|
||||
|
||||
## 4.0.8
|
||||
|
||||
### Adds:
|
||||
|
||||
- Upload: Distant files can be added via their URL in GUI and by API. Phraseanet downloads the file before archiving it.
|
||||
- Search optimisation when searching in full text, there was a problem when the query mixed different types of fields.
|
||||
- Search optimisation, it’s now possible to search a partial date in full text.
|
||||
- Populate optimisation, now populating time: 3 times faster.
|
||||
- It is now possible to migrate from 3.1 3.0 version to 4.X, without an intermediate step in 3.8.Fix:
|
||||
|
||||
### Fixes
|
||||
|
||||
- Search filter were not taken into account due to a bug in JS.
|
||||
- Overlay title: In this field, text was repeated twice if : one or several words were highlighted in the field, and if the title contained more than 102 characters.
|
||||
- List Manager: it was impossible to add users in the list manager after page 3.
|
||||
- List of fields was not refreshed in the exported fields section.
|
||||
- Push and Feedback fix error when adding a user when Geonames was not set (null value in Geonames).
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Adds:
|
||||
|
||||
- Advanced search refacto
|
||||
- Thesaurus search is now in strict mode
|
||||
- Refactoring of report module
|
||||
- Refactoring query storage and changing strategy for field search restriction
|
||||
- It is now possible to search for terms in thesaurus and candidates in all languages, not only on the login language
|
||||
- Enhancements on archive task
|
||||
- Graphic enhancements for menu and icons
|
||||
- Video file enhancement, support of MXF container
|
||||
- Extraction of a video soundtrack (MP3, MP4, WAVE, etc.)
|
||||
- For Office Documents, all generated subviews will be PDF assets by default. The flexpaper preview still exists but will be optional.
|
||||
- In Prod Gui, there will be 5 facets but the possibility to view more.
|
||||
|
||||
### Fixes:
|
||||
|
||||
- Quarantine: Fix for the “Substitute” action: alert when selection is empty
|
||||
- Quarantine: File name with a special character can’t be added
|
||||
- Fix for the Adobe CC default token
|
||||
- XSS vulnerabilities in Prod, Admin & Lightbox. Many thanks to Kris (@HV_hat_)
|
||||
- PDF containing (XMP-xmp:PageImage) fails generating subview
|
||||
- MIME types are trucated
|
||||
-Vagrant dev environment fix
|
||||
- Feedback: Sort assets “Order by best choice” has no effect
|
||||
|
||||
## 4.0.3
|
||||
|
||||
### Adds:
|
||||
|
||||
- Prod: For a record, show the current day in the statistics section of the detailed view.
|
||||
- Prod: Store state (open or closed) of facet answer. eg: Database or collection, store in session.
|
||||
- Admin: Access to scheduler and task local menu when parameter is set to false in .yml configuration.
|
||||
- Prod: Database, collection and document type facets are fixed on top
|
||||
- Prod: Better rendering for values of exposure, shutter speed and flash status in facets. eg for shutter speed: 1/30 instead of 0,0333333.
|
||||
- Versions 4 are now compliant with the Phraseanet plugins for Adobe CC Suite.
|
||||
- White list mode: extending autoregistration and adding wildcard access condition by mail domain. Automatically grant access to a user according to the email entered in the request.
|
||||
- Find your documents from the colors in the facets (AI plugin)
|
||||
- Generate a PDF from a Word document or a picture, it’s now possible to define a pdf subview type
|
||||
- Specify a temporary work repository for building video subdefs, to accelerate video generation.
|
||||
|
||||
### Fixes:
|
||||
|
||||
- Prod: In Upload, correct status are not loaded
|
||||
- Prod:Arrow keys navigation adds last selected facet as filter
|
||||
- Admin:Subdef presets, sizes and bitrates (bits/s) not OK
|
||||
- Admin: App error on loading in French due to a simple quote
|
||||
- Prod: Deletion message is not fully readable when deleting a story
|
||||
- Fixing highlight with Elasticsearch for full text only, not for the thesaurus
|
||||
- 500 error at the first authentication for a user with the SAML Phraseanet pluginDev
|
||||
- Dev: Fix API version returned in answer
|
||||
- Dev: Fix vagrant provisioning for Windows
|
||||
|
||||
## 4.0.2
|
||||
|
||||
### Adds:
|
||||
|
||||
- Prod: Message Improv, when selected records are in Trash and another one.
|
||||
- Prod: alt-click on active facets (filter) to invert it.
|
||||
- Prod: do not erase facets in filter when returning 0 answers.
|
||||
- Core: Add preference to authorize user connection without an email
|
||||
- Core: Add preference to set default validity period of download link
|
||||
|
||||
### Fixes:
|
||||
|
||||
- Thesaurus: 0 character terms are blocked
|
||||
- Admin: fix action create and drop index from elasticsearch
|
||||
- Prod: Fix advanced sarch: no filters possible on fields using IE
|
||||
- Prod: 500 error in publication reader when record is missing (deleted from db)Unit test: fix error in Json serialization for custom link
|
||||
- Prod: fix field list in advanced search with Edge browser
|
||||
- Upload: fix 500 error when missing collection
|
||||
- Install wizard: fix error in graphical installer
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Adds:
|
||||
|
||||
#### Phraseanet gets a new search engine: Elasticsearch
|
||||
- Faceted navigation enables to create a “mapping” of the response. Browse in a very intuitive way by creating several associations of filters. Facets can be used on the databases, collections, documentary fields and technical data.
|
||||
- Speed of processing search and results display has been improved
|
||||
- Possibility to use Kibana (open source visualization plugin for Elasticsearch)
|
||||
|
||||
#### API enhancement
|
||||
- New API routes are available (orders, facets, quarantine)
|
||||
- Enhancement of new, faster routes
|
||||
|
||||
#### Redesign of the Prod interface
|
||||
- Enhanced, redesigned ergonomics: the detailed view windows; redesign of the workzone (baskets and stories, facets, webgalleries)
|
||||
- New white and grey skins are now available
|
||||
- New order manager
|
||||
|
||||
#### Other
|
||||
- Permalinks sharing: activate/deactivate sharing links for the document and sub resolutions
|
||||
- New: the applicative trash: you can now define a collection named _TRASH_. Then, all deleted records from collections (except from Trash) go to the Trash collection. Permalinks on subdefs are deactivated. When you delete a record from the Trash collection, it is permanently deleted. When you move a record from the Trash collection to another, the permalinks are reactivated.
|
||||
- Rewriting of the task scheduler based on the web sockets
|
||||
- Quarantine enhancement
|
||||
- Drag and drop upload
|
||||
|
||||
## 3.8.8 (2015-12-02)
|
||||
|
||||
|
@@ -40,7 +40,7 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
|
||||
&& php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
|
||||
&& php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
|
||||
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
|
||||
&& php -r "unlink('composer-setup.php');"
|
||||
|
||||
@@ -88,7 +88,7 @@ COPY templates /var/alchemy/templates
|
||||
COPY tests /var/alchemy/tests
|
||||
|
||||
# Phraseanet
|
||||
FROM php:7.0-fpm-stretch as phraseanet
|
||||
FROM php:7.0-fpm-stretch as phraseanet-fpm
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
apt-transport-https \
|
||||
@@ -152,6 +152,10 @@ WORKDIR /var/alchemy/Phraseanet
|
||||
ENTRYPOINT ["/phraseanet-entrypoint.sh"]
|
||||
CMD ["/boot.sh"]
|
||||
|
||||
# phraseanet-worker
|
||||
FROM phraseanet-fpm as phraseanet-worker
|
||||
CMD ["/worker-boot.sh"]
|
||||
|
||||
# phraseanet-nginx
|
||||
FROM nginx:1.15 as phraseanet-nginx
|
||||
RUN useradd -u 1000 app
|
||||
|
34
README.md
@@ -10,6 +10,10 @@ Phraseanet 4.1 - Digital Asset Management application
|
||||
- Elasticsearch search engine
|
||||
- Multiple resolution assets generation
|
||||
|
||||
# License :
|
||||
|
||||
Phraseanet is licensed under GPL-v3 license.
|
||||
|
||||
# Documentation :
|
||||
|
||||
https://docs.phraseanet.com/
|
||||
@@ -31,34 +35,42 @@ https://www.phraseanet.com/download/
|
||||
# Development :
|
||||
|
||||
For development purpose Phraseanet is shipped with ready to use development environments using vagrant.
|
||||
You can easily choose betweeen a complete build or a prebuild box, with a specific PHP version.
|
||||
|
||||
- git clone
|
||||
- vagrant up
|
||||
git clone
|
||||
vagrant up --provision
|
||||
|
||||
then, a prompt allow you to choose PHP version, and another one to choose a complete build or an Alchemy prebuilt boxes.
|
||||
|
||||
Ex:
|
||||
- vagrant up --provision //// 5.6 ///// 1 >> Build an ubuntu/xenial box with php5.6
|
||||
- vagrant up --provision //// 7.0 ///// 1 >> Build an ubuntu/xenial with php7.0
|
||||
- vagrant up --provision //// 7.2 ///// 2 >> Build the alchemy/phraseanet-php-7.2 box
|
||||
- vagrant up --provision //// 5.6 ///// 1 >> Build the alchemy/phraseanet-php-5.6 box
|
||||
|
||||
|
||||
For development with Phraseanet API see https://docs.phraseanet.com/4.0/en/Devel/index.html
|
||||
|
||||
# License :
|
||||
|
||||
Phraseanet is licensed under GPL-v3 license.
|
||||
|
||||
|
||||
# Docker build
|
||||
|
||||
WARNING : still in a work-in-progress status and can be used only for test purposes.
|
||||
|
||||
The docker distribution come with 2 differents containers :
|
||||
* an nginx that act as the front http server.
|
||||
* the php-fpm who serves the php files through nginx.
|
||||
The docker distribution come with 3 differents containers :
|
||||
* An nginx that act as the front http server.
|
||||
* The php-fpm who serves the php files through nginx.
|
||||
* The worker who execute Phraseanet scheduler.
|
||||
|
||||
## How to build
|
||||
|
||||
The two images can be built respectively with these two commands :
|
||||
The three images can be built respectively with these commands :
|
||||
|
||||
# nginx server
|
||||
docker build --target phraseanet-nginx -t local/phraseanet-nginx .
|
||||
|
||||
# php-fpm application
|
||||
docker build --target phraseanet -t local/phraseanet .
|
||||
docker build --target phraseanet-fpm -t local/phraseanet-fpm .
|
||||
|
||||
# worker
|
||||
docker build --target phraseanet-worker -t local/phraseanet-worker .
|
||||
|
||||
|
110
Vagrantfile
vendored
@@ -1,16 +1,6 @@
|
||||
Vagrant.require_version ">= 1.5"
|
||||
require 'json'
|
||||
|
||||
class MyCustomError < StandardError
|
||||
attr_reader :code
|
||||
|
||||
def initialize(code)
|
||||
@code = code
|
||||
end
|
||||
|
||||
def to_s
|
||||
"[#{code} #{super}]"
|
||||
end
|
||||
end
|
||||
# Check to determine whether we're on a windows or linux/os-x host,
|
||||
# later on we use this to launch ansible in the supported way
|
||||
# source: https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
||||
@@ -34,15 +24,62 @@ else if which('ifconfig')
|
||||
end
|
||||
end
|
||||
|
||||
$php = [ "5.6", "7.0", "7.1", "7.2" ]
|
||||
$phpVersion = ENV['phpversion'] ? ENV['phpversion'] : "7.0";
|
||||
|
||||
unless Vagrant.has_plugin?('vagrant-hostmanager')
|
||||
raise "vagrant-hostmanager is not installed! Please run\n vagrant plugin install vagrant-hostmanager\n\n"
|
||||
end
|
||||
|
||||
unless $php.include?($phpVersion)
|
||||
raise "You should specify php version before running vagrant\n\n (Available : 5.6, 7.0, 7.1, 7.2 | default => 5.6)\n\n Exemple: phpversion='7.0' vagrant up \n\n"
|
||||
# Check to determine if box_meta JSON is present
|
||||
# if provisionned : pick name of box
|
||||
if File.file?(".vagrant/machines/default/virtualbox/box_meta")
|
||||
data = File.read(".vagrant/machines/default/virtualbox/box_meta")
|
||||
parsed_json = JSON.parse(data)
|
||||
$box = parsed_json["name"]
|
||||
end
|
||||
|
||||
# if not : run prompt to configure provisioning
|
||||
if !File.file?(".vagrant/machines/default/virtualbox/box_meta") && ARGV[0] == 'up'
|
||||
print "\033[34m \nChoose a Build type :\n\n(1) Use prebuilt Phraseanet Box\n(2) Build Phraseanet from scratch (xenial)\n\033[00m"
|
||||
type = STDIN.gets.chomp
|
||||
print "\n"
|
||||
# Switch between Phraseanet box and native trusty64
|
||||
case (type)
|
||||
when '1'
|
||||
$box = "alchemy/Phraseanet-vagrant-dev_php"
|
||||
$playbook = "resources/ansible/playbook-boxes.yml"
|
||||
when '2'
|
||||
$box = "ubuntu/xenial64"
|
||||
$playbook = "resources/ansible/playbook.yml"
|
||||
else
|
||||
raise "\033[31mYou should specify Build type before running vagrant\n\n (Available : 1, 2)\n\n\033[00m"
|
||||
end
|
||||
print "\033[32m-----------------------------------------------\n"
|
||||
print "Build with "+$box+" box\n"
|
||||
print "-----------------------------------------------\n\n\033[00m"
|
||||
|
||||
print "\033[34mChoose a PHP version for your build (Available : 5.6, 7.0, 7.1, 7.2)\n\033[00m"
|
||||
phpversion = STDIN.gets.chomp
|
||||
print "\n"
|
||||
# Php version selection
|
||||
case (phpversion)
|
||||
when "5.6", "7.0", "7.1", "7.2"
|
||||
print "\033[32mSelected PHP version : "+phpversion+"\n\033[00m"
|
||||
print "Continue ? (Y/n) \n"
|
||||
continue = STDIN.gets.chomp
|
||||
case continue
|
||||
when 'n', 'no', 'N', 'NO'
|
||||
raise "\033[31mBuild aborted\033[00m"
|
||||
else
|
||||
if (type == '1')
|
||||
$box.concat(phpversion)
|
||||
end
|
||||
print "\033[32m-----------------------------------------------\n"
|
||||
print "Build with PHP"+phpversion+"\n"
|
||||
print "-----------------------------------------------\n\n\033[00m"
|
||||
|
||||
end
|
||||
else
|
||||
raise "\033[31mYou should specify php version before running vagrant\n\n (Available : 5.6, 7.0, 7.1, 7.2)\n\n\033[00m"
|
||||
end
|
||||
end
|
||||
|
||||
$root = File.dirname(File.expand_path(__FILE__))
|
||||
@@ -95,8 +132,6 @@ else if $env == "linux"
|
||||
$hostIps = `ifconfig | sed -nE 's/[[:space:]]*inet ([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(.*)$/\\1/p'`.split("\n");
|
||||
else
|
||||
$hostIps = `resources/ansible/inventories/GetIpAdresses.cmd`;
|
||||
# raise MyCustomError.new($hostIps), "HOST IP"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,33 +154,28 @@ Vagrant.configure("2") do |config|
|
||||
]
|
||||
end
|
||||
|
||||
# Switch between Phraseanet box and native trusty64
|
||||
config.vm.box = "alchemy/Phraseanet-vagrant-dev"
|
||||
#config.vm.box = "ubuntu/trusty64"
|
||||
|
||||
# In case, Phraseanet box, choose the php version
|
||||
# For php 7.0 use box 0.0.1
|
||||
# For php 7.1 use box 0.0.2
|
||||
config.vm.box_version = "0.0.1"
|
||||
|
||||
config.vm.box = $box
|
||||
config.ssh.forward_agent = true
|
||||
config_net(config)
|
||||
|
||||
# If ansible is in your path it will provision from your HOST machine
|
||||
# If ansible is not found in the path it will be instaled in the VM and provisioned from there
|
||||
if which('ansible-playbook')
|
||||
config.vm.provision "ansible_local" do |ansible|
|
||||
ansible.playbook = "resources/ansible/playbook.yml"
|
||||
ansible.limit = 'all'
|
||||
ansible.verbose = 'vvv'
|
||||
ansible.extra_vars = {
|
||||
hostname: $hostname,
|
||||
host_addresses: $hostIps,
|
||||
phpversion: $phpVersion,
|
||||
postfix: {
|
||||
postfix_domain: $hostname + ".vb"
|
||||
|
||||
if $playbook
|
||||
config.vm.provision "ansible_local" do |ansible|
|
||||
ansible.playbook = $playbook
|
||||
ansible.limit = 'all'
|
||||
ansible.verbose = 'vvv'
|
||||
ansible.extra_vars = {
|
||||
hostname: $hostname,
|
||||
host_addresses: $hostIps,
|
||||
phpversion: phpversion,
|
||||
postfix: {
|
||||
postfix_domain: $hostname + ".vb"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
config.vm.provision "ansible_local", run: "always" do |ansible|
|
||||
@@ -158,10 +188,6 @@ Vagrant.configure("2") do |config|
|
||||
}
|
||||
end
|
||||
else
|
||||
# raise MyCustomError.new([$hostname, $phpVersion, $hostIps]), "HOST IP"
|
||||
# raise MyCustomError.new($hostIps), "HOST IP"
|
||||
# raise MyCustomError.new($hostIps), "HOST IP"
|
||||
|
||||
config.vm.provision :shell, path: "resources/ansible/windows.sh", args: [$hostname, $phpVersion, $hostIps]
|
||||
# config.vm.provision :shell, run: "always", path: "resources/ansible/windows-always.sh", args: ["default"]
|
||||
end
|
||||
|
@@ -24,6 +24,7 @@ use Alchemy\Phrasea\Command\Plugin\AddPlugin;
|
||||
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
|
||||
use Alchemy\Phrasea\Command\Plugin\EnablePlugin;
|
||||
use Alchemy\Phrasea\Command\Plugin\DisablePlugin;
|
||||
use Alchemy\Phrasea\Command\Plugin\DownloadPlugin;
|
||||
use Alchemy\Phrasea\CLI;
|
||||
use Alchemy\Phrasea\Command\Setup\CheckEnvironment;
|
||||
use Alchemy\Phrasea\Core\CLIProvider\DoctrineMigrationServiceProvider;
|
||||
@@ -50,7 +51,7 @@ $app = new CLI("
|
||||
This program comes with ABSOLUTELY NO WARRANTY.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `about:license' for details.\n\n"
|
||||
. ' SETUP', $version->getName() . ' ' . $version->getNumber());
|
||||
. ' SETUP', $version->getName() . ' ' . $version->getNumber());
|
||||
|
||||
$app->register(new DoctrineMigrationServiceProvider());
|
||||
|
||||
@@ -70,6 +71,7 @@ if ($configurationTester->isInstalled()) {
|
||||
}
|
||||
|
||||
$app->command(new AddPlugin());
|
||||
$app->command(new DownloadPlugin());
|
||||
$app->command(new ListPlugin());
|
||||
$app->command(new RemovePlugin());
|
||||
$app->command(new PluginsReset());
|
||||
|
0
cache/.gitkeep
vendored
@@ -47,7 +47,7 @@
|
||||
"php": ">=5.5.9",
|
||||
"ext-intl": "*",
|
||||
"alchemy-fr/tcpdf-clone": "~6.0",
|
||||
"alchemy/embed-bundle": "^2.0.4",
|
||||
"alchemy/embed-bundle": "^2.0.7",
|
||||
"alchemy/geonames-api-consumer": "~0.1.0",
|
||||
"alchemy/mediavorus": "^0.4.4",
|
||||
"alchemy/oauth2php": "1.1.0",
|
||||
@@ -120,7 +120,8 @@
|
||||
"google/recaptcha": "^1.1",
|
||||
"facebook/graph-sdk": "^5.6",
|
||||
"box/spout": "^2.7",
|
||||
"paragonie/random-lib": "^2.0"
|
||||
"paragonie/random-lib": "^2.0",
|
||||
"czproject/git-php": "^3.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsstream": "~1.5",
|
||||
|
66
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a40bfa0aa6310530dc0c92b141b21305",
|
||||
"content-hash": "f3b1fc0a30bf14b05e57ce673550d9c0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "alchemy-fr/tcpdf-clone",
|
||||
@@ -131,16 +131,16 @@
|
||||
},
|
||||
{
|
||||
"name": "alchemy/embed-bundle",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alchemy-fr/embed-bundle.git",
|
||||
"reference": "b510748686c05c0c1d59b7ad15e2c1098abafc5a"
|
||||
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/b510748686c05c0c1d59b7ad15e2c1098abafc5a",
|
||||
"reference": "b510748686c05c0c1d59b7ad15e2c1098abafc5a",
|
||||
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
|
||||
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -178,10 +178,10 @@
|
||||
],
|
||||
"description": "Embed resources bundle",
|
||||
"support": {
|
||||
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.4",
|
||||
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.7",
|
||||
"issues": "https://github.com/alchemy-fr/embed-bundle/issues"
|
||||
},
|
||||
"time": "2019-06-03T13:35:50+00:00"
|
||||
"time": "2019-09-02T12:28:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "alchemy/geonames-api-consumer",
|
||||
@@ -383,16 +383,16 @@
|
||||
},
|
||||
{
|
||||
"name": "alchemy/phpexiftool",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alchemy-fr/PHPExiftool.git",
|
||||
"reference": "7372ca4e43473328bf06bca810558fbad7bb2f95"
|
||||
"reference": "ba1cb51eceb6562d7996023478977a8739de188b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/7372ca4e43473328bf06bca810558fbad7bb2f95",
|
||||
"reference": "7372ca4e43473328bf06bca810558fbad7bb2f95",
|
||||
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/ba1cb51eceb6562d7996023478977a8739de188b",
|
||||
"reference": "ba1cb51eceb6562d7996023478977a8739de188b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -452,7 +452,7 @@
|
||||
"exiftool",
|
||||
"metadata"
|
||||
],
|
||||
"time": "2017-05-18T19:04:04+00:00"
|
||||
"time": "2019-02-13T13:06:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "alchemy/queue-bundle",
|
||||
@@ -1156,6 +1156,48 @@
|
||||
],
|
||||
"time": "2016-08-09T20:10:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
"version": "v3.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/czproject/git-php.git",
|
||||
"reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/czproject/git-php/zipball/a7b911b81a2fe626f748a4ac8955353c5777bc6c",
|
||||
"reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Pecha",
|
||||
"email": "janpecha@email.cz"
|
||||
}
|
||||
],
|
||||
"description": "Library for work with Git repository in PHP.",
|
||||
"keywords": [
|
||||
"git"
|
||||
],
|
||||
"time": "2019-02-09T13:11:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dailymotion/sdk",
|
||||
"version": "1.6.5",
|
||||
|
@@ -231,17 +231,19 @@ embed_bundle:
|
||||
video:
|
||||
player: videojs
|
||||
autoplay: false
|
||||
coverSubdef: previewx4
|
||||
available-speeds:
|
||||
cover_subdef: thumbnail
|
||||
message_start: StartOfMessage
|
||||
available_speeds:
|
||||
- 1
|
||||
- 1.5
|
||||
- 3
|
||||
audio:
|
||||
player: videojs
|
||||
autoplay: false
|
||||
cover_subdef: thumbnail
|
||||
document:
|
||||
player: flexpaper
|
||||
enable-pdfjs: true
|
||||
enable_pdfjs: true
|
||||
geocoding-providers:
|
||||
-
|
||||
map-provider: mapboxWebGL
|
||||
|
3
docker/phraseanet/worker-boot.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
runuser app -c 'php /var/alchemy/Phraseanet/bin/console task-manager:scheduler:run'
|
@@ -115,6 +115,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Process\ExecutableFinder;
|
||||
use Unoconv\UnoconvServiceProvider;
|
||||
use XPDF\PdfToText;
|
||||
use XPDF\XPDFServiceProvider;
|
||||
@@ -237,8 +238,19 @@ class Application extends SilexApplication
|
||||
|
||||
$this->register(new UnicodeServiceProvider());
|
||||
$this->register(new ValidatorServiceProvider());
|
||||
$this->register(new XPDFServiceProvider());
|
||||
$this->setupXpdf();
|
||||
|
||||
if ($this['configuration.store']->isSetup()) {
|
||||
$binariesConfig = $this['conf']->get(['main', 'binaries']);
|
||||
$executableFinder = new ExecutableFinder();
|
||||
$this->register(new XPDFServiceProvider(), [
|
||||
'xpdf.configuration' => [
|
||||
'pdftotext.binaries' => isset($binariesConfig['pdftotext_binary']) ? $binariesConfig['pdftotext_binary'] : $executableFinder->find('pdftotext'),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->setupXpdf();
|
||||
}
|
||||
|
||||
$this->register(new FileServeServiceProvider());
|
||||
$this->register(new ManipulatorServiceProvider());
|
||||
$this->register(new PluginServiceProvider());
|
||||
@@ -653,7 +665,7 @@ class Application extends SilexApplication
|
||||
private function setupGeonames()
|
||||
{
|
||||
$this['geonames.server-uri'] = $this->share(function (Application $app) {
|
||||
return $app['conf']->get(['registry', 'webservices', 'geonames-server'], 'http://geonames.alchemyasp.com/');
|
||||
return $app['conf']->get(['registry', 'webservices', 'geonames-server'], 'https://geonames.alchemyasp.com/');
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -51,4 +51,41 @@ abstract class AbstractPluginCommand extends Command
|
||||
$this->container['plugins.autoloader-generator']->write($manifests);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
}
|
||||
|
||||
protected function doInstallPlugin($source, InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
|
||||
|
||||
$output->write("Importing <info>$source</info>...");
|
||||
$this->container['plugins.importer']->import($source, $temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Validating plugin...");
|
||||
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment> found <info>".$manifest->getName()."</info>");
|
||||
|
||||
$targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName();
|
||||
|
||||
$output->write("Setting up composer...");
|
||||
$this->container['plugins.composer-installer']->install($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
|
||||
$this->container['filesystem']->mirror($temporaryDir, $targetDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Copying public files <info>".$manifest->getName()."</info>...");
|
||||
$this->container['plugins.assets-manager']->update($manifest);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Removing temporary directory...");
|
||||
$this->container['filesystem']->remove($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Activating plugin...");
|
||||
$this->container['conf']->set(['plugins', $manifest->getName(), 'enabled'], true);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$this->updateConfigFiles($input, $output);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Command\Plugin;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
|
||||
class AddPlugin extends AbstractPluginCommand
|
||||
{
|
||||
@@ -29,41 +30,36 @@ class AddPlugin extends AbstractPluginCommand
|
||||
protected function doExecutePluginAction(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$source = $input->getArgument('source');
|
||||
$shouldDownload = $this->shouldDownloadPlugin($source);
|
||||
|
||||
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
|
||||
if ($shouldDownload){
|
||||
$command = $this->getApplication()->find('plugins:download');
|
||||
$arguments = [
|
||||
'command' => 'plugins:download',
|
||||
'source' => $source,
|
||||
'shouldInstallPlugin' => true
|
||||
];
|
||||
|
||||
$output->write("Importing <info>$source</info>...");
|
||||
$this->container['plugins.importer']->import($source, $temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
$downloadInput = new ArrayInput($arguments);
|
||||
$command->run($downloadInput, $output);
|
||||
|
||||
$output->write("Validating plugin...");
|
||||
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment> found <info>".$manifest->getName()."</info>");
|
||||
} else {
|
||||
|
||||
$targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName();
|
||||
|
||||
$output->write("Setting up composer...");
|
||||
$this->container['plugins.composer-installer']->install($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
|
||||
$this->container['filesystem']->mirror($temporaryDir, $targetDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Copying public files <info>".$manifest->getName()."</info>...");
|
||||
$this->container['plugins.assets-manager']->update($manifest);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Removing temporary directory...");
|
||||
$this->container['filesystem']->remove($temporaryDir);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$output->write("Activating plugin...");
|
||||
$this->container['conf']->set(['plugins', $manifest->getName(), 'enabled'], true);
|
||||
$output->writeln(" <comment>OK</comment>");
|
||||
|
||||
$this->updateConfigFiles($input, $output);
|
||||
$this->doInstallPlugin($source, $input, $output);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function shouldDownloadPlugin($source)
|
||||
{
|
||||
$allowedScheme = array('https','ssh');
|
||||
|
||||
$scheme = parse_url($source, PHP_URL_SCHEME);
|
||||
if (in_array($scheme, $allowedScheme)){
|
||||
return true;
|
||||
} else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
157
lib/Alchemy/Phrasea/Command/Plugin/DownloadPlugin.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Phraseanet
|
||||
*
|
||||
* (c) 2005-2016 Alchemy
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Alchemy\Phrasea\Command\Plugin;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Cz\Git\GitRepository as GitRepository;
|
||||
|
||||
class DownloadPlugin extends AbstractPluginCommand
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('plugins:download');
|
||||
|
||||
$this
|
||||
->setDescription('Downloads a plugin to Phraseanet')
|
||||
->addArgument('source', InputArgument::REQUIRED, 'The source is a remote url (.zip or .git)')
|
||||
->addArgument('destination', InputArgument::OPTIONAL, 'Download destination')
|
||||
->addArgument('shouldInstallPlugin', InputArgument::OPTIONAL, 'True or false, determines if plugin should be installed after download');
|
||||
}
|
||||
|
||||
protected function doExecutePluginAction(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$source = $input->getArgument('source');
|
||||
$destination = $input->getArgument('destination');
|
||||
$shouldInstallPlugin = false;
|
||||
$shouldInstallPlugin = $input->getArgument('shouldInstallPlugin');
|
||||
|
||||
$destinationSubdir = '/plugin-'.md5($source);
|
||||
|
||||
if ($destination){
|
||||
|
||||
$destination = trim($destination);
|
||||
$destination = rtrim($destination, '/');
|
||||
|
||||
$localDownloadPath = $destination;
|
||||
|
||||
} else {
|
||||
|
||||
$localDownloadPath = '/tmp/plugin-download' . $destinationSubdir;
|
||||
}
|
||||
|
||||
if (!is_dir($localDownloadPath)) {
|
||||
mkdir($localDownloadPath, 0755, true);
|
||||
}
|
||||
|
||||
$extension = $this->getURIExtension($source);
|
||||
|
||||
if ($extension){
|
||||
|
||||
switch ($extension){
|
||||
|
||||
case 'zip':
|
||||
|
||||
$localUnpackPath = '/tmp/plugin-zip'. $destinationSubdir;
|
||||
|
||||
if (!is_dir($localUnpackPath)) {
|
||||
mkdir($localUnpackPath, 0755, true);
|
||||
}
|
||||
|
||||
$localArchiveFile = $localUnpackPath . '/plugin-downloaded.zip';
|
||||
|
||||
// download
|
||||
$output->writeln("Downloading <info>$source</info>...");
|
||||
set_time_limit(0);
|
||||
$fp = fopen ($localArchiveFile, 'w+');
|
||||
$ch = curl_init($source);;
|
||||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
// unpack
|
||||
$output->writeln("Unpacking <info>$source</info>...");
|
||||
$zip = new \ZipArchive();
|
||||
$errorUnpack = false;
|
||||
|
||||
if ($zip->open($localArchiveFile)) {
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
if (!($zip->extractTo($localDownloadPath, array($zip->getNameIndex($i))))) {
|
||||
$errorUnpack = true;
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
if ($errorUnpack){
|
||||
$output->writeln("Failed unzipping <info>$source</info>");
|
||||
} else {
|
||||
$output->writeln("Plugin downloaded to <info>$localDownloadPath</info>");
|
||||
if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output);
|
||||
}
|
||||
|
||||
// remove zip archive
|
||||
$this->delDirTree($localUnpackPath);
|
||||
|
||||
break;
|
||||
|
||||
case 'git':
|
||||
$output->writeln("Downloading <info>$source</info>...");
|
||||
$repo = GitRepository::cloneRepository($source, $localDownloadPath);
|
||||
$output->writeln("Plugin downloaded to <info>$localDownloadPath</info>");
|
||||
if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$output->writeln("The source <info>$source</info> is not supported. Only .zip and .git are supported.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function getURIExtension($source)
|
||||
{
|
||||
$validExtension = false;
|
||||
$allowedExtension = array('zip','git');
|
||||
|
||||
$path = parse_url($source, PHP_URL_PATH);
|
||||
if (strpos($path, '.') !== false) {
|
||||
$pathParts = explode('.', $path);
|
||||
$extension = $pathParts[1];
|
||||
if (in_array($extension, $allowedExtension)){
|
||||
$validExtension = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($validExtension){
|
||||
return $extension;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function delDirTree($dir) {
|
||||
$files = array_diff(scandir($dir), array('.','..'));
|
||||
foreach ($files as $file) {
|
||||
(is_dir("$dir/$file")) ? self::delDirTree("$dir/$file") : unlink("$dir/$file");
|
||||
}
|
||||
return rmdir($dir);
|
||||
}
|
||||
}
|
@@ -944,7 +944,7 @@ class V1Controller extends Controller
|
||||
}
|
||||
|
||||
$originalName = $pi['filename'] . '.' . $pi['extension'];
|
||||
$newPathname = $tempfile;
|
||||
$uploadedFilename = $newPathname = $tempfile;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -956,8 +956,11 @@ class V1Controller extends Controller
|
||||
if (!$file->isValid()) {
|
||||
return $this->getBadRequestAction($request, 'Data corrupted, please try again');
|
||||
}
|
||||
|
||||
$uploadedFilename = $file->getPathname();
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$newPathname = $file->getPathname() . '.' . $file->getClientOriginalExtension();
|
||||
|
||||
if (false === rename($file->getPathname(), $newPathname)) {
|
||||
return Result::createError($request, 403, 'Error while renaming file')->createResponse();
|
||||
}
|
||||
@@ -1010,6 +1013,11 @@ class V1Controller extends Controller
|
||||
$nosubdef = $request->get('nosubdefs') === '' || \p4field::isyes($request->get('nosubdefs'));
|
||||
$this->getBorderManager()->process($session, $Package, $callback, $behavior, $nosubdef);
|
||||
|
||||
// remove $newPathname on temporary directory
|
||||
if ($newPathname !== $uploadedFilename) {
|
||||
@rename($newPathname, $uploadedFilename);
|
||||
}
|
||||
|
||||
$ret = ['entity' => null];
|
||||
|
||||
if ($output instanceof \record_adapter) {
|
||||
@@ -1081,6 +1089,11 @@ class V1Controller extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// remove $newPathname on temporary directory
|
||||
if ($renamedFilename !== $uploadedFilename) {
|
||||
@rename($renamedFilename, $uploadedFilename);
|
||||
}
|
||||
|
||||
return Result::create($request, $ret)->createResponse();
|
||||
}
|
||||
|
||||
@@ -1984,7 +1997,7 @@ class V1Controller extends Controller
|
||||
return $this->getBadRequestAction($request);
|
||||
}
|
||||
|
||||
$datas = substr($datas, 0, ($n)) . $value . substr($datas, ($n + 2));
|
||||
$datas = substr($datas, 0, ($n)) . $value . substr($datas, ($n + 1));
|
||||
}
|
||||
|
||||
$record->setStatus(strrev($datas));
|
||||
@@ -2588,8 +2601,18 @@ class V1Controller extends Controller
|
||||
foreach ($recordsData as $data) {
|
||||
$records[] = $this->addOrDelStoryRecord($story, $data, $action);
|
||||
if($action === 'ADD' && !$cover_set && isset($data->{'use_as_cover'}) && $data->{'use_as_cover'} === true) {
|
||||
$coverSource = [];
|
||||
|
||||
if (isset($data->{'thumbnail_cover_source'})) {
|
||||
$coverSource['thumbnail_cover_source'] = $data->{'thumbnail_cover_source'};
|
||||
}
|
||||
|
||||
if (isset($data->{'preview_cover_source'})) {
|
||||
$coverSource['preview_cover_source'] = $data->{'preview_cover_source'};
|
||||
}
|
||||
|
||||
// because we can try many records as cover source, we let it fail
|
||||
$cover_set = ($this->setStoryCover($story, $data->{'record_id'}, true) !== false);
|
||||
$cover_set = ($this->setStoryCover($story, $data->{'record_id'}, true, $coverSource) !== false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2643,14 +2666,26 @@ class V1Controller extends Controller
|
||||
|
||||
$story = new \record_adapter($this->app, $databox_id, $story_id);
|
||||
|
||||
$coverSource = [];
|
||||
|
||||
if (isset($data->{'thumbnail_cover_source'})) {
|
||||
$coverSource['thumbnail_cover_source'] = $data->{'thumbnail_cover_source'};
|
||||
}
|
||||
|
||||
if (isset($data->{'preview_cover_source'})) {
|
||||
$coverSource['preview_cover_source'] = $data->{'preview_cover_source'};
|
||||
}
|
||||
|
||||
// we do NOT let "setStoryCover()" fail : pass false as last arg
|
||||
$record_key = $this->setStoryCover($story, $data->{'record_id'}, false);
|
||||
$record_key = $this->setStoryCover($story, $data->{'record_id'}, false, $coverSource);
|
||||
|
||||
return Result::create($request, array($record_key))->createResponse();
|
||||
}
|
||||
|
||||
protected function setStoryCover(\record_adapter $story, $record_id, $can_fail=false)
|
||||
protected function setStoryCover(\record_adapter $story, $record_id, $can_fail=false, $coverSource = [])
|
||||
{
|
||||
$coverSource = array_merge(['thumbnail_cover_source' => 'thumbnail', 'preview_cover_source' => 'preview'], $coverSource);
|
||||
|
||||
try {
|
||||
$record = new \record_adapter($this->app, $story->getDataboxId(), $record_id);
|
||||
} catch (\Exception_Record_AdapterNotFound $e) {
|
||||
@@ -2662,18 +2697,22 @@ class V1Controller extends Controller
|
||||
$this->app->abort(404, sprintf('Record identified by databox_id %s and record_id %s is not in the story', $story->getDataboxId(), $record_id));
|
||||
}
|
||||
|
||||
if ($record->getType() !== 'image' && $record->getType() !== 'video') {
|
||||
// this can fail so we can loop on many records during story creation...
|
||||
if($can_fail) {
|
||||
return false;
|
||||
}
|
||||
$this->app->abort(403, sprintf('Record identified by databox_id %s and record_id %s is not an image nor a video', $story->getDataboxId(), $record_id));
|
||||
}
|
||||
// taking account all record type as a cover
|
||||
// if ($record->getType() !== 'image' && $record->getType() !== 'video') {
|
||||
// // this can fail so we can loop on many records during story creation...
|
||||
// if($can_fail) {
|
||||
// return false;
|
||||
// }
|
||||
// $this->app->abort(403, sprintf('Record identified by databox_id %s and record_id %s is not an image nor a video', $story->getDataboxId(), $record_id));
|
||||
// }
|
||||
|
||||
foreach ($record->get_subdefs() as $name => $value) {
|
||||
if (!in_array($name, array('thumbnail', 'preview'))) {
|
||||
if (!($key = array_search($name, $coverSource))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = ($key == 'thumbnail_cover_source') ? 'thumbnail': 'preview';
|
||||
|
||||
$media = $this->app->getMediaFromUri($value->getRealPath());
|
||||
$this->getSubdefSubstituer()->substituteSubdef($story, $name, $media); // name = thumbnail | preview
|
||||
$this->getDataboxLogger($story->getDatabox())->log(
|
||||
|
@@ -114,6 +114,7 @@ class MoveCollectionController extends Controller
|
||||
$trashCollectionsBySbasId = [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$oldCollectionId = $record->getCollection()->get_coll_id();
|
||||
$record->move_to_collection($collection, $this->getApplicationBox());
|
||||
|
||||
if ($request->request->get("chg_coll_son") == "1") {
|
||||
@@ -130,7 +131,7 @@ class MoveCollectionController extends Controller
|
||||
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
|
||||
}
|
||||
if ($trashCollectionsBySbasId[$sbasId] !== null) {
|
||||
if ($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id() && $collection->get_coll_id() !== $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
|
||||
if ($oldCollectionId == $trashCollectionsBySbasId[$sbasId]->get_coll_id() && $collection->get_coll_id() !== $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
|
||||
// record is already in trash so active it
|
||||
foreach ($record->get_subdefs() as $subdef) {
|
||||
if (($pl = $subdef->get_permalink())) {
|
||||
|
@@ -463,6 +463,8 @@ class PushController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$manager = $this->getEntityManager();
|
||||
|
||||
$password = $this->getRandomGenerator()->generateString(128);
|
||||
|
||||
$user = $this->getUserManipulator()->createUser($email, $password, $email);
|
||||
@@ -476,12 +478,15 @@ class PushController extends Controller
|
||||
$user->setCompany($request->request->get('company'));
|
||||
}
|
||||
if ($request->request->get('job')) {
|
||||
$user->setCompany($request->request->get('job'));
|
||||
$user->setJob($request->request->get('job'));
|
||||
}
|
||||
if ($request->request->get('form_geonameid')) {
|
||||
$this->getUserManipulator()->setGeonameId($user, $request->request->get('form_geonameid'));
|
||||
if ($request->request->get('city')) {
|
||||
$this->getUserManipulator()->setGeonameId($user, $request->request->get('city'));
|
||||
}
|
||||
|
||||
$manager->persist($user);
|
||||
$manager->flush();
|
||||
|
||||
$result['message'] = $this->app->trans('User successfully created');
|
||||
$result['success'] = true;
|
||||
$result['user'] = $this->formatUser($user);
|
||||
|
@@ -179,7 +179,6 @@ class QueryController extends Controller
|
||||
};
|
||||
|
||||
$userManipulator->setUserSetting($user, 'last_jsonquery', (string)$request->request->get('jsQuery'));
|
||||
|
||||
$jsQuery = @json_decode((string)$request->request->get('jsQuery'), true);
|
||||
if(($ft = $findFulltext($jsQuery['query'])) !== null) {
|
||||
$userManipulator->setUserSetting($user, 'start_page_query', $ft);
|
||||
@@ -215,7 +214,7 @@ class QueryController extends Controller
|
||||
if (min($d2top, $d2bottom) < 4) {
|
||||
if ($d2bottom < 4) {
|
||||
if($page != 1){
|
||||
$string .= "<a id='PREV_PAGE' class='btn btn-primary btn-mini'></a>";
|
||||
$string .= "<a id='PREV_PAGE' class='btn btn-primary btn-mini icon-baseline-chevron_left-24px'></a>";
|
||||
}
|
||||
for ($i = 1; ($i <= 4 && (($i <= $npages) === true)); $i++) {
|
||||
if ($i == $page)
|
||||
@@ -224,13 +223,13 @@ class QueryController extends Controller
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
|
||||
}
|
||||
if ($npages > 4)
|
||||
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini'></a>";
|
||||
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action" data-page="' . $npages . '" id="last"></a>';
|
||||
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini icon icon-baseline-chevron_right-24px'></a>";
|
||||
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action icon icon-double-arrows" data-page="' . $npages . '" id="last"></a>';
|
||||
} else {
|
||||
$start = $npages - 4;
|
||||
if (($start) > 0){
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"></a>';
|
||||
$string .= '<a id="PREV_PAGE" class="btn btn-primary btn-mini"></a>';
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"><span class="icon icon-double-arrows icon-inverse"></span></a>';
|
||||
$string .= '<a id="PREV_PAGE" class="btn btn-primary btn-mini icon icon-baseline-chevron_left-24px"></a>';
|
||||
}else
|
||||
$start = 1;
|
||||
for ($i = ($start); $i <= $npages; $i++) {
|
||||
@@ -240,11 +239,11 @@ class QueryController extends Controller
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
|
||||
}
|
||||
if($page < $npages){
|
||||
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini'></a>";
|
||||
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini icon icon-baseline-chevron_right-24px'></a>";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$string .= '<a class="btn btn-primary btn-mini btn-mini search-navigate-action" data-page="1" id="first"></a>';
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"><span class="icon icon-double-arrows icon-inverse"></span></a>';
|
||||
|
||||
for ($i = ($page - 2); $i <= ($page + 2); $i++) {
|
||||
if ($i == $page)
|
||||
@@ -253,10 +252,10 @@ class QueryController extends Controller
|
||||
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
|
||||
}
|
||||
|
||||
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action" data-page="' . $npages . '" id="last"></a>';
|
||||
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action icon icon-double-arrows" data-page="' . $npages . '" id="last"></a>';
|
||||
}
|
||||
}
|
||||
$string .= '<div style="display:none;"><div id="NEXT_PAGE"></div><div id="PREV_PAGE"></div></div>';
|
||||
$string .= '<div style="display:none;"><div id="NEXT_PAGE" class="icon icon-baseline-chevron_right-24px"></div><div id="PREV_PAGE" class="icon icon-baseline-chevron_left-24px"></div></div>';
|
||||
|
||||
$explain = $this->render(
|
||||
"prod/results/infos.html.twig",
|
||||
@@ -317,7 +316,7 @@ class QueryController extends Controller
|
||||
</tfoot>
|
||||
</table></div></div>'
|
||||
. '</div><a href="#" class="search-display-info" data-infos="' . str_replace('"', '"', $explain) . '">'
|
||||
. $this->app->trans('%total% reponses', ['%total%' => '<span>'.$result->getTotal().'</span>']) . '</a>';
|
||||
. $this->app->trans('%total% reponses', ['%total%' => '<span>'.number_format($result->getTotal(),null, null, ' ').'</span>']) . '</a>';
|
||||
|
||||
$json['infos'] = $infoResult;
|
||||
$json['navigationTpl'] = $string;
|
||||
@@ -471,7 +470,6 @@ class QueryController extends Controller
|
||||
$json['results'] = $this->render($template, ['results'=> $result]);
|
||||
}
|
||||
|
||||
|
||||
return $this->app->json($json);
|
||||
}
|
||||
|
||||
|
@@ -15,12 +15,11 @@ use Alchemy\Phrasea\Core\Configuration\DisplaySettingService;
|
||||
use Alchemy\Phrasea\Exception\SessionNotFound;
|
||||
use Alchemy\Phrasea\Feed\Aggregate;
|
||||
use Alchemy\Phrasea\Helper;
|
||||
use Alchemy\Phrasea\Model\Entities\UserSetting;
|
||||
use Alchemy\Phrasea\Helper\WorkZone as WorkzoneHelper;
|
||||
use Alchemy\Phrasea\Model\Repositories\FeedRepository;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
||||
class RootController extends Controller
|
||||
{
|
||||
use Application\Helper\FirewallAware;
|
||||
@@ -41,12 +40,11 @@ class RootController extends Controller
|
||||
public function indexAction(Request $request) {
|
||||
try {
|
||||
\Session_Logger::updateClientInfos($this->app, 1);
|
||||
} catch (SessionNotFound $e) {
|
||||
}
|
||||
catch (SessionNotFound $e) {
|
||||
return $this->app->redirectPath('logout');
|
||||
}
|
||||
|
||||
$css = [];
|
||||
|
||||
$user = $this->getAuthenticatedUser();
|
||||
$cssfile = $this->getSettings()->getUserSetting($user, 'css');
|
||||
|
||||
@@ -85,6 +83,22 @@ class RootController extends Controller
|
||||
/** @var \Closure $filter */
|
||||
$filter = $this->app['plugin.filter_by_authorization'];
|
||||
|
||||
/* prepare work to extend whole taskbar... later
|
||||
$menus = [
|
||||
'push' => ['native'=>true, 'n'=>0],
|
||||
'tools' => ['native'=>true, 'n'=>0],
|
||||
];
|
||||
/ ** @var ActionBarPluginInterface $plugin * /
|
||||
foreach($filter('actionbar') as $kplugin=>$plugin) {
|
||||
foreach($plugin->getActionBar() as $kmenu=>$menu) {
|
||||
if(!array_key_exists($kmenu, $menus)) {
|
||||
$menus[$kmenu] = ['native'=>false, 'n'=>0];
|
||||
}
|
||||
$menus[$kmenu]['n']++;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
$plugins = [
|
||||
'workzone' => $filter('workzone'),
|
||||
'actionbar' => $filter('actionbar'),
|
||||
@@ -92,7 +106,7 @@ class RootController extends Controller
|
||||
|
||||
return $this->render('prod/index.html.twig', [
|
||||
'module_name' => 'Production',
|
||||
'WorkZone' => new Helper\WorkZone($this->app, $request),
|
||||
'WorkZone' => new WorkzoneHelper($this->app, $request),
|
||||
'module_prod' => $helper,
|
||||
'search_datas' => $helper->get_search_datas(),
|
||||
'cssfile' => $cssfile,
|
||||
@@ -105,7 +119,7 @@ class RootController extends Controller
|
||||
'feeds' => $feeds,
|
||||
'aggregate' => $aggregate,
|
||||
'GV_google_api' => $conf->get(['registry', 'webservices', 'google-charts-enabled']),
|
||||
'geocodingProviders' => $conf->get(['geocoding-providers']),
|
||||
'geocodingProviders' => $conf->get(['geocoding-providers']),
|
||||
'search_status' => \databox_status::getSearchStatus($this->app),
|
||||
'thesau_js_list' => $thjslist,
|
||||
'thesau_json_sbas' => json_encode($sbas),
|
||||
|
@@ -367,15 +367,17 @@ class UploadController extends Controller
|
||||
$postMaxSize = PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$r = 0;
|
||||
switch (strtolower(substr($postMaxSize, -1))) {
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'g':
|
||||
$postMaxSize *= 1024;
|
||||
$r += 10;
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'm':
|
||||
$postMaxSize *= 1024;
|
||||
$r += 10;
|
||||
case 'k':
|
||||
$postMaxSize *= 1024;
|
||||
$r += 10;
|
||||
$postMaxSize = ((int)($postMaxSize))<<$r;
|
||||
}
|
||||
|
||||
return min(UploadedFile::getMaxFilesize(), (int) $postMaxSize);
|
||||
|
@@ -522,27 +522,35 @@ class AccountController extends Controller
|
||||
|
||||
$list = array_keys($this->app['repo.collections-registry']->getBaseIdMap());
|
||||
|
||||
$this->app->getAclForUser($user)->revoke_access_from_bases($list);
|
||||
try {
|
||||
$this->app->getAclForUser($user)->revoke_access_from_bases($list);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// one or more access could not be revoked ? the user will not be phantom
|
||||
$this->app->addFlash('error', $this->app->trans('phraseanet::error: failed to revoke some user access'));
|
||||
}
|
||||
|
||||
if ($this->app->getAclForUser($user)->is_phantom()) {
|
||||
// send confirmation email: the account has been deleted
|
||||
|
||||
try {
|
||||
$receiver = Receiver::fromUser($user);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->app->addFlash('error', $this->app->trans('phraseanet::erreur: echec du serveur de mail'));
|
||||
$mail = MailSuccessAccountDelete::create($this->app, $receiver);
|
||||
}
|
||||
catch (InvalidArgumentException $e) {
|
||||
$this->app->addFlash('error', $this->app->trans('phraseanet::erreur: echec du serveur de mail'));
|
||||
$mail = null;
|
||||
}
|
||||
|
||||
$mail = MailSuccessAccountDelete::create($this->app, $receiver);
|
||||
|
||||
$this->app['manipulator.user']->delete($user);
|
||||
|
||||
$this->deliver($mail);
|
||||
if($mail) {
|
||||
$this->deliver($mail);
|
||||
}
|
||||
|
||||
$this->getAuthenticator()->closeAccount();
|
||||
$this->app->addFlash('info', $this->app->trans('phraseanet::account The account has been deleted'));
|
||||
}
|
||||
|
||||
$this->getAuthenticator()->closeAccount();
|
||||
$this->app->addFlash('info', $this->app->trans('phraseanet::account The account has been deleted'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -265,7 +265,7 @@ class LoginController extends Controller
|
||||
return $this->render('login/register-classic.html.twig', array_merge(
|
||||
$this->getDefaultTemplateVariables($request),
|
||||
[
|
||||
'geonames_server_uri' => str_replace(sprintf('%s:', parse_url($url, PHP_URL_SCHEME)), '', $url),
|
||||
'geonames_server_uri' => $url,
|
||||
'form' => $form->createView()
|
||||
]));
|
||||
}
|
||||
|
@@ -94,10 +94,9 @@ class SessionController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Check session state
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws \Exception in case "new \DateTime()" fails ?
|
||||
*/
|
||||
public function updateSession(Request $request)
|
||||
{
|
||||
@@ -120,7 +119,8 @@ class SessionController extends Controller
|
||||
|
||||
return $this->app->json($ret);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$ret['status'] = 'disconnected';
|
||||
|
||||
return $this->app->json($ret);
|
||||
@@ -128,7 +128,8 @@ class SessionController extends Controller
|
||||
|
||||
try {
|
||||
$this->getApplicationBox()->get_connection();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
return $this->app->json($ret);
|
||||
}
|
||||
|
||||
@@ -148,8 +149,9 @@ class SessionController extends Controller
|
||||
$module->setModuleId($moduleId);
|
||||
$module->setSession($session);
|
||||
$manager->persist($module);
|
||||
} else {
|
||||
$manager->persist($session->getModuleById($moduleId)->setUpdated(new \DateTime()));
|
||||
}
|
||||
else {
|
||||
$manager->persist($session->getModuleById($moduleId)->setUpdated($now));
|
||||
}
|
||||
|
||||
$manager->persist($session);
|
||||
@@ -231,7 +233,10 @@ class SessionController extends Controller
|
||||
*/
|
||||
private function getBasketRepository()
|
||||
{
|
||||
return $this->getEntityManager()->getRepository('Phraseanet:Basket');
|
||||
/** @var BasketRepository $ret */
|
||||
$ret = $this->getEntityManager()->getRepository('Phraseanet:Basket');
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -146,7 +146,7 @@ class RegistryFormManipulator
|
||||
],
|
||||
'webservices' => [
|
||||
'google-charts-enabled' => true,
|
||||
'geonames-server' => 'http://geonames.alchemyasp.com/',
|
||||
'geonames-server' => 'https://geonames.alchemyasp.com/',
|
||||
'captchas-enabled' => false,
|
||||
'recaptcha-public-key' => '',
|
||||
'recaptcha-private-key' => '',
|
||||
|
@@ -16,7 +16,7 @@ class Version
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $number = '4.1.0-alpha.14a';
|
||||
private $number = '4.1.0-alpha.15a';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
@@ -11,28 +11,29 @@
|
||||
|
||||
namespace Alchemy\Phrasea\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Alchemy\Phrasea\Model\Entities\Basket as BasketEntity;
|
||||
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class WorkZone extends Helper
|
||||
{
|
||||
const BASKETS = 'baskets';
|
||||
const STORIES = 'stories';
|
||||
const BASKETS = 'baskets';
|
||||
const STORIES = 'stories';
|
||||
const VALIDATIONS = 'validations';
|
||||
|
||||
/**
|
||||
*
|
||||
* Returns an ArrayCollection containing three keys :
|
||||
* - self::BASKETS : an ArrayCollection of the actives baskets
|
||||
* (Non Archived)
|
||||
* - self::BASKETS : an ArrayCollection of the actives baskets (Non Archived)
|
||||
* - self::STORIES : an ArrayCollection of working stories
|
||||
* - self::VALIDATIONS : the validation people are waiting from me
|
||||
*
|
||||
* @return \Doctrine\Common\Collections\ArrayCollection
|
||||
* @param null|string $sort "date"|"name"
|
||||
* @return ArrayCollection
|
||||
*/
|
||||
public function getContent($sort)
|
||||
public function getContent($sort = null)
|
||||
{
|
||||
/* @var $repo_baskets Alchemy\Phrasea\Model\Repositories\BasketRepository */
|
||||
/* @var $repo_baskets BasketRepository */
|
||||
$repo_baskets = $this->app['repo.baskets'];
|
||||
|
||||
$sort = in_array($sort, ['date', 'name']) ? $sort : 'name';
|
||||
@@ -42,7 +43,7 @@ class WorkZone extends Helper
|
||||
$baskets = $repo_baskets->findActiveByUser($this->app->getAuthenticatedUser(), $sort);
|
||||
|
||||
// force creation of a default basket
|
||||
if (0 === count($baskets)) {
|
||||
if (count($baskets) === 0) {
|
||||
$basket = new BasketEntity();
|
||||
|
||||
$basket->setName($this->app->trans('Default basket'));
|
||||
@@ -55,7 +56,7 @@ class WorkZone extends Helper
|
||||
|
||||
$validations = $repo_baskets->findActiveValidationByUser($this->app->getAuthenticatedUser(), $sort);
|
||||
|
||||
/* @var $repo_stories Alchemy\Phrasea\Model\Repositories\StoryWZRepository */
|
||||
/* @var $repo_stories StoryWZRepository */
|
||||
$repo_stories = $this->app['repo.story-wz'];
|
||||
|
||||
$stories = $repo_stories->findByUser($this->app, $this->app->getAuthenticatedUser(), $sort);
|
||||
|
@@ -61,7 +61,7 @@ class SubdefGenerator
|
||||
|
||||
public function generateSubdefs(\record_adapter $record, array $wanted_subdefs = null)
|
||||
{
|
||||
if ($record->get_hd_file() !== null) {
|
||||
if ($record->get_hd_file() !== null && $record->get_hd_file()->getMimeType() == "application/x-indesign") {
|
||||
$mediaSource = $this->mediavorus->guess($record->get_hd_file()->getPathname());
|
||||
$metadatas = $mediaSource->getMetadatas();
|
||||
|
||||
@@ -69,15 +69,27 @@ class SubdefGenerator
|
||||
if(!isset($this->tmpFilesystem)){
|
||||
$this->tmpFilesystem = Manager::create();
|
||||
}
|
||||
$tmpDir = $this->tmpFilesystem->createTemporaryDirectory();
|
||||
$tmpDir = $this->tmpFilesystem->createTemporaryDirectory(0777, 500);
|
||||
|
||||
try {
|
||||
$this->app['filesystem']->dumpFile($tmpDir.'/file.jpg', $metadatas->get('XMP-xmp:PageImage')->getValue()->asString());
|
||||
$this->tmpFilePath = $tmpDir.'/file.jpg';
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error(sprintf('Unable to write temporary file : %s', $e->getMessage()));
|
||||
$files = $this->app['exiftool.preview-extractor']->extract($record->get_hd_file()->getPathname(), $tmpDir);
|
||||
|
||||
$selected = null;
|
||||
$size = null;
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file->isDir() || $file->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_null($selected) || $file->getSize() > $size) {
|
||||
$selected = $file->getPathname();
|
||||
$size = $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
if ($selected) {
|
||||
$this->tmpFilePath = $selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -73,6 +73,9 @@ class SubdefSubstituer
|
||||
|
||||
$this->createMediaSubdef($record, 'document', $media);
|
||||
|
||||
$record->setMimeType($media->getFile()->getMimeType());
|
||||
$record->setType($media->getType());
|
||||
|
||||
$record->write_metas();
|
||||
|
||||
if ($shouldSubdefsBeRebuilt) {
|
||||
|
@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Metadata;
|
||||
use Alchemy\Phrasea\Border\File;
|
||||
use Alchemy\Phrasea\Databox\DataboxRepository;
|
||||
use Alchemy\Phrasea\Metadata\Tag\NoSource;
|
||||
use DateTime;
|
||||
use PHPExiftool\Driver\Metadata\Metadata;
|
||||
|
||||
class PhraseanetMetadataSetter
|
||||
@@ -66,8 +67,16 @@ class PhraseanetMetadataSetter
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['value'] = $value;
|
||||
if ($field->get_type() == 'date') {
|
||||
try {
|
||||
$dateTime = new DateTime($value);
|
||||
$value = $dateTime->format('Y/m/d H:i:s');
|
||||
} catch (\Exception $e) {
|
||||
// $value unchanged
|
||||
}
|
||||
}
|
||||
|
||||
$data['value'] = $value;
|
||||
$metadataInRecordFormat[] = $data;
|
||||
}
|
||||
}
|
||||
|
@@ -54,21 +54,24 @@ class BasketRepository extends EntityRepository
|
||||
/**
|
||||
* Returns all basket for a given user that are not marked as archived
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @param null|string $sort
|
||||
* @return Basket[]
|
||||
*/
|
||||
public function findActiveByUser(User $user, $sort = null)
|
||||
{
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
LEFT JOIN b.elements e
|
||||
WHERE b.user = :usr_id
|
||||
AND b.archived = false';
|
||||
// checked : 4 usages, "b.elements" is useless
|
||||
$dql = "SELECT b\n"
|
||||
. " FROM Phraseanet:Basket b\n"
|
||||
// . " LEFT JOIN b.elements e\n" //
|
||||
. " WHERE b.user = :usr_id\n"
|
||||
. " AND b.archived = false";
|
||||
|
||||
if ($sort == 'date') {
|
||||
$dql .= ' ORDER BY b.created DESC';
|
||||
} elseif ($sort == 'name') {
|
||||
$dql .= ' ORDER BY b.name ASC';
|
||||
$dql .= "\n ORDER BY b.created DESC";
|
||||
}
|
||||
elseif ($sort == 'name') {
|
||||
$dql .= "\n ORDER BY b.name ASC";
|
||||
}
|
||||
|
||||
$query = $this->_em->createQuery($dql);
|
||||
@@ -80,24 +83,27 @@ class BasketRepository extends EntityRepository
|
||||
/**
|
||||
* Returns all unread basket for a given user that are not marked as archived
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @return Basket[]
|
||||
*/
|
||||
public function findUnreadActiveByUser(User $user)
|
||||
{
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
LEFT JOIN b.validation s
|
||||
LEFT JOIN s.participants p
|
||||
WHERE b.archived = false
|
||||
AND (
|
||||
(b.user = :usr_id_owner AND b.isRead = false)
|
||||
OR (b.user != :usr_id_ownertwo
|
||||
AND p.user = :usr_id_participant
|
||||
AND p.is_aware = false)
|
||||
)
|
||||
AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP())';
|
||||
// checked : 2 usages, "b.elements" is useless
|
||||
$dql = "SELECT b\n"
|
||||
. " FROM Phraseanet:Basket b\n"
|
||||
// . " JOIN b.elements e\n"
|
||||
. " LEFT JOIN b.validation s\n"
|
||||
. " LEFT JOIN s.participants p\n"
|
||||
. " WHERE b.archived = false\n"
|
||||
. " AND (\n"
|
||||
. " (b.user = :usr_id_owner AND b.isRead = false)\n"
|
||||
. " OR \n"
|
||||
. " (b.user != :usr_id_ownertwo\n"
|
||||
. " AND p.user = :usr_id_participant\n"
|
||||
. " AND p.is_aware = false\n"
|
||||
. " AND s.expires > CURRENT_TIMESTAMP()\n"
|
||||
. " )\n"
|
||||
. " )";
|
||||
|
||||
$params = [
|
||||
'usr_id_owner' => $user->getId(),
|
||||
@@ -115,11 +121,22 @@ class BasketRepository extends EntityRepository
|
||||
* Returns all baskets that are in validation session not expired and
|
||||
* where a specified user is participant (not owner)
|
||||
*
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @param null|string $sort
|
||||
* @return Basket[]
|
||||
*/
|
||||
public function findActiveValidationByUser(User $user, $sort = null)
|
||||
{
|
||||
// checked : 2 usages, "b.elements" seems useless.
|
||||
$dql = "SELECT b\n"
|
||||
. "FROM Phraseanet:Basket b\n"
|
||||
// . " JOIN b.elements e\n"
|
||||
// . " JOIN e.validation_datas v\n"
|
||||
. " JOIN b.validation s\n"
|
||||
. " JOIN s.participants p\n"
|
||||
. "WHERE b.user != ?1 AND p.user = ?2\n"
|
||||
. " AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP())";
|
||||
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
@@ -130,9 +147,9 @@ class BasketRepository extends EntityRepository
|
||||
AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP()) ';
|
||||
|
||||
if ($sort == 'date') {
|
||||
$dql .= ' ORDER BY b.created DESC';
|
||||
$dql .= "\nORDER BY b.created DESC";
|
||||
} elseif ($sort == 'name') {
|
||||
$dql .= ' ORDER BY b.name ASC';
|
||||
$dql .= "\nORDER BY b.name ASC";
|
||||
}
|
||||
|
||||
$query = $this->_em->createQuery($dql);
|
||||
@@ -152,10 +169,11 @@ class BasketRepository extends EntityRepository
|
||||
*/
|
||||
public function findUserBasket($basket_id, User $user, $requireOwner)
|
||||
{
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
LEFT JOIN b.elements e
|
||||
WHERE b.id = :basket_id';
|
||||
// checked : 3 usages, "b.elements e" seems useless
|
||||
$dql = "SELECT b\n"
|
||||
. " FROM Phraseanet:Basket b\n"
|
||||
// . " LEFT JOIN b.elements e\n"
|
||||
. " WHERE b.id = :basket_id";
|
||||
|
||||
$query = $this->_em->createQuery($dql);
|
||||
$query->setParameters(['basket_id' => $basket_id]);
|
||||
@@ -188,7 +206,7 @@ class BasketRepository extends EntityRepository
|
||||
|
||||
public function findContainingRecordForUser(\record_adapter $record, User $user)
|
||||
{
|
||||
|
||||
// todo : check "e.sbas_id = e.sbas_id" ???
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
@@ -210,30 +228,31 @@ class BasketRepository extends EntityRepository
|
||||
{
|
||||
switch ($type) {
|
||||
case self::RECEIVED:
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
WHERE b.user = :usr_id AND b.pusher_id IS NOT NULL';
|
||||
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
|
||||
$dql = "SELECT b\n"
|
||||
. "FROM Phraseanet:Basket b\n"
|
||||
. " JOIN b.elements e\n"
|
||||
. "WHERE b.user = :usr_id AND b.pusher_id IS NOT NULL";
|
||||
$params = [
|
||||
'usr_id' => $user->getId()
|
||||
];
|
||||
break;
|
||||
case self::VALIDATION_DONE:
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
JOIN b.validation s
|
||||
JOIN s.participants p
|
||||
WHERE b.user != ?1 AND p.user = ?2';
|
||||
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
|
||||
$dql = "SELECT b\n"
|
||||
. "FROM Phraseanet:Basket b\n"
|
||||
. " JOIN b.elements e\n"
|
||||
. " JOIN b.validation s\n"
|
||||
. " JOIN s.participants p\n"
|
||||
. "WHERE b.user != ?1 AND p.user = ?2";
|
||||
$params = [
|
||||
1 => $user->getId()
|
||||
, 2 => $user->getId()
|
||||
1 => $user->getId(),
|
||||
2 => $user->getId()
|
||||
];
|
||||
break;
|
||||
case self::VALIDATION_SENT:
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
JOIN b.elements e
|
||||
JOIN b.validation v
|
||||
WHERE b.user = :usr_id';
|
||||
$params = [
|
||||
@@ -243,7 +262,6 @@ class BasketRepository extends EntityRepository
|
||||
case self::MYBASKETS:
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
LEFT JOIN b.elements e
|
||||
LEFT JOIN b.validation s
|
||||
LEFT JOIN s.participants p
|
||||
WHERE (b.user = :usr_id)';
|
||||
@@ -252,6 +270,7 @@ class BasketRepository extends EntityRepository
|
||||
];
|
||||
break;
|
||||
default:
|
||||
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
LEFT JOIN b.elements e
|
||||
@@ -297,6 +316,7 @@ class BasketRepository extends EntityRepository
|
||||
*/
|
||||
public function findActiveValidationAndBasketByUser(User $user, $sort = null)
|
||||
{
|
||||
// todo : check caller and if "LEFT JOIN b.elements e" is usefull
|
||||
$dql = 'SELECT b
|
||||
FROM Phraseanet:Basket b
|
||||
LEFT JOIN b.elements e
|
||||
|
@@ -30,6 +30,11 @@ class FieldKey implements Key, QueryPostProcessor
|
||||
return $this->getField($context)->getIndexField($raw);
|
||||
}
|
||||
|
||||
public function getFieldType(QueryContext $context)
|
||||
{
|
||||
return $this->getField($context)->getType();
|
||||
}
|
||||
|
||||
public function isValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
return ValueChecker::isValueCompatible($this->getField($context), $value);
|
||||
|
@@ -6,6 +6,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
interface Key
|
||||
{
|
||||
public function getFieldType(QueryContext $context);
|
||||
public function getIndexField(QueryContext $context, $raw = false);
|
||||
public function isValueCompatible($value, QueryContext $context);
|
||||
public function __toString();
|
||||
|
@@ -23,6 +23,11 @@ class MetadataKey implements Key
|
||||
return $this->getTag($context)->getIndexField($raw);
|
||||
}
|
||||
|
||||
public function getFieldType(QueryContext $context)
|
||||
{
|
||||
return $this->getTag($context)->getType();
|
||||
}
|
||||
|
||||
public function isValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
return ValueChecker::isValueCompatible($this->getTag($context), $value);
|
||||
|
@@ -52,6 +52,11 @@ class NativeKey implements Key
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
public function getFieldType(QueryContext $context)
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getIndexField(QueryContext $context, $raw = false)
|
||||
{
|
||||
return $this->key;
|
||||
|
@@ -2,18 +2,20 @@
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
use Assert\Assertion;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor;
|
||||
|
||||
class RangeExpression extends Node
|
||||
{
|
||||
/** @var FieldKey */
|
||||
private $key;
|
||||
|
||||
private $lower_bound;
|
||||
private $lower_inclusive;
|
||||
private $higher_bound;
|
||||
@@ -55,20 +57,34 @@ class RangeExpression extends Node
|
||||
public function buildQuery(QueryContext $context)
|
||||
{
|
||||
$params = array();
|
||||
if ($this->lower_bound !== null) {
|
||||
$this->assertValueCompatible($this->lower_bound, $context);
|
||||
if ($this->lower_inclusive) {
|
||||
$params['gte'] = $this->lower_bound;
|
||||
} else {
|
||||
$params['gt'] = $this->lower_bound;
|
||||
/** @var StructureField $field */
|
||||
// $field = $this->key->getField($context);
|
||||
$lower_bound = $this->lower_bound;
|
||||
$higher_bound = $this->higher_bound;
|
||||
|
||||
if($this->key->getFieldType($context) === FieldMapping::TYPE_DATE) {
|
||||
if($lower_bound !== null) {
|
||||
$lower_bound = RecordHelper::sanitizeDate($lower_bound);
|
||||
}
|
||||
if($higher_bound !== null) {
|
||||
$higher_bound = RecordHelper::sanitizeDate($higher_bound);
|
||||
}
|
||||
}
|
||||
if ($this->higher_bound !== null) {
|
||||
$this->assertValueCompatible($this->higher_bound, $context);
|
||||
if ($this->higher_inclusive) {
|
||||
$params['lte'] = $this->higher_bound;
|
||||
|
||||
if ($lower_bound !== null) {
|
||||
$this->assertValueCompatible($lower_bound, $context);
|
||||
if ($this->lower_inclusive) {
|
||||
$params['gte'] = $lower_bound;
|
||||
} else {
|
||||
$params['lt'] = $this->higher_bound;
|
||||
$params['gt'] = $lower_bound;
|
||||
}
|
||||
}
|
||||
if ($higher_bound !== null) {
|
||||
$this->assertValueCompatible($higher_bound, $context);
|
||||
if ($this->higher_inclusive) {
|
||||
$params['lte'] = $higher_bound;
|
||||
} else {
|
||||
$params['lt'] = $higher_bound;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,11 @@ class TimestampKey implements Key, Typed
|
||||
return FieldMapping::TYPE_DATE;
|
||||
}
|
||||
|
||||
public function getFieldType(QueryContext $context)
|
||||
{
|
||||
return FieldMapping::TYPE_DATE;
|
||||
}
|
||||
|
||||
public function getIndexField(QueryContext $context, $raw = false)
|
||||
{
|
||||
return $this->index_field;
|
||||
|
@@ -396,10 +396,10 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
if ($options->getDateFields() && ($options->getMaxDate() || $options->getMinDate())) {
|
||||
$range = [];
|
||||
if ($options->getMaxDate()) {
|
||||
$range['lte'] = $options->getMaxDate()->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
|
||||
$range['lte'] = $options->getMaxDate()->format('Y-m-d');
|
||||
}
|
||||
if ($options->getMinDate()) {
|
||||
$range['gte'] = $options->getMinDate()->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
|
||||
$range['gte'] = $options->getMinDate()->format('Y-m-d');
|
||||
}
|
||||
|
||||
foreach ($options->getDateFields() as $dateField) {
|
||||
|
@@ -16,8 +16,7 @@ class FieldMapping
|
||||
|
||||
const DATE_FORMAT_MYSQL = 'yyyy-MM-dd HH:mm:ss';
|
||||
const DATE_FORMAT_CAPTION = 'yyyy/MM/dd'; // ES format
|
||||
const DATE_FORMAT_MYSQL_OR_CAPTION = 'yyyy-MM-dd HH:mm:ss||yyyy/MM/dd';
|
||||
const DATE_FORMAT_CAPTION_PHP = 'Y/m/d'; // PHP format
|
||||
const DATE_FORMAT_MYSQL_OR_CAPTION = 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||yyyy-MM||yyyy';
|
||||
|
||||
// Core types
|
||||
const TYPE_STRING = 'string';
|
||||
|
@@ -155,15 +155,16 @@ class BulkOperation
|
||||
// nb: results (items) are returned IN THE SAME ORDER as commands were pushed in the stack
|
||||
// so the items[X] match the operationIdentifiers[X]
|
||||
foreach ($response['items'] as $key => $item) {
|
||||
foreach($item as $command=>$result) { // command may be "index" or "delete"
|
||||
if($response['errors'] && $result['status'] >= 400) { // 4xx or 5xx error
|
||||
throw new Exception(sprintf('%d: %s', $key, var_export($result, true)));
|
||||
foreach ($item as $command=>$result) { // command may be "index" or "delete"
|
||||
if ($response['errors'] && $result['status'] >= 400) { // 4xx or 5xx
|
||||
$err = array_key_exists('error', $result) ? var_export($result['error'], true) : ($command . " error " . $result['status']);
|
||||
throw new Exception(sprintf('%d: %s', $key, $err));
|
||||
}
|
||||
}
|
||||
|
||||
$operationIdentifier = $this->operationIdentifiers[$key];
|
||||
|
||||
if(is_string($operationIdentifier) || is_int($operationIdentifier)) { // dont include null keys
|
||||
if (is_string($operationIdentifier) || is_int($operationIdentifier)) { // dont include null keys
|
||||
$callbackData[$operationIdentifier] = $response['items'][$key];
|
||||
}
|
||||
}
|
||||
|
@@ -39,18 +39,13 @@ class MetadataHydrator implements HydratorInterface
|
||||
|
||||
public function hydrateRecords(array &$records)
|
||||
{
|
||||
$sql = <<<SQL
|
||||
(SELECT record_id, ms.name AS `key`, m.value AS value, 'caption' AS type, ms.business AS private
|
||||
FROM metadatas AS m
|
||||
INNER JOIN metadatas_structure AS ms ON (ms.id = m.meta_struct_id)
|
||||
WHERE record_id IN (?))
|
||||
|
||||
UNION
|
||||
|
||||
(SELECT record_id, t.name AS `key`, t.value AS value, 'exif' AS type, 0 AS private
|
||||
FROM technical_datas AS t
|
||||
WHERE record_id IN (?))
|
||||
SQL;
|
||||
$sql = "(SELECT record_id, ms.name AS `key`, m.value AS value, 'caption' AS type, ms.business AS private\n"
|
||||
. " FROM metadatas AS m INNER JOIN metadatas_structure AS ms ON (ms.id = m.meta_struct_id)\n"
|
||||
. " WHERE record_id IN (?))\n"
|
||||
. "UNION\n"
|
||||
. "(SELECT record_id, t.name AS `key`, t.value AS value, 'exif' AS type, 0 AS private\n"
|
||||
. " FROM technical_datas AS t\n"
|
||||
. " WHERE record_id IN (?))\n";
|
||||
|
||||
$ids = array_keys($records);
|
||||
$statement = $this->connection->executeQuery(
|
||||
@@ -62,7 +57,7 @@ SQL;
|
||||
while ($metadata = $statement->fetch()) {
|
||||
// Store metadata value
|
||||
$key = $metadata['key'];
|
||||
$value = $metadata['value'];
|
||||
$value = trim($metadata['value']);
|
||||
|
||||
// Do not keep empty values
|
||||
if ($key === '' || $value === '') {
|
||||
@@ -80,7 +75,7 @@ SQL;
|
||||
case 'caption':
|
||||
// Sanitize fields
|
||||
$value = StringHelper::crlfNormalize($value);
|
||||
$value = $this->sanitizeValue($value, $this->structure->typeOf($key));
|
||||
$value = $this->helper->sanitizeValue($value, $this->structure->typeOf($key));
|
||||
// Private caption fields are kept apart
|
||||
$type = $metadata['private'] ? 'private_caption' : 'caption';
|
||||
// Caption are multi-valued
|
||||
@@ -103,7 +98,7 @@ SQL;
|
||||
}
|
||||
$tag = $this->structure->getMetadataTagByName($key);
|
||||
if ($tag) {
|
||||
$value = $this->sanitizeValue($value, $tag->getType());
|
||||
$value = $this->helper->sanitizeValue($value, $tag->getType());
|
||||
}
|
||||
// EXIF data is single-valued
|
||||
$record['metadata_tags'][$key] = $value;
|
||||
@@ -118,33 +113,6 @@ SQL;
|
||||
$this->clearGpsPositionBuffer();
|
||||
}
|
||||
|
||||
private function sanitizeValue($value, $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case FieldMapping::TYPE_STRING:
|
||||
return str_replace("\0", "", $value);
|
||||
|
||||
case FieldMapping::TYPE_DATE:
|
||||
return $this->helper->sanitizeDate($value);
|
||||
|
||||
case FieldMapping::TYPE_FLOAT:
|
||||
case FieldMapping::TYPE_DOUBLE:
|
||||
return (float) $value;
|
||||
|
||||
case FieldMapping::TYPE_INTEGER:
|
||||
case FieldMapping::TYPE_LONG:
|
||||
case FieldMapping::TYPE_SHORT:
|
||||
case FieldMapping::TYPE_BYTE:
|
||||
return (int) $value;
|
||||
|
||||
case FieldMapping::TYPE_BOOLEAN:
|
||||
return (bool) $value;
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleGpsPosition(&$records, $id, $tag_name, $value)
|
||||
{
|
||||
// Get position object
|
||||
|
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\Connection as DriverConnection;
|
||||
|
||||
@@ -18,31 +20,34 @@ class TitleHydrator implements HydratorInterface
|
||||
{
|
||||
private $connection;
|
||||
|
||||
public function __construct(DriverConnection $connection)
|
||||
/** @var RecordHelper */
|
||||
private $helper;
|
||||
|
||||
public function __construct(DriverConnection $connection, RecordHelper $helper)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
public function hydrateRecords(array &$records)
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
m.`record_id`,
|
||||
CASE ms.`thumbtitle`
|
||||
WHEN "1" THEN "default"
|
||||
WHEN "0" THEN "default"
|
||||
ELSE ms.`thumbtitle`
|
||||
END AS locale,
|
||||
CASE ms.`thumbtitle`
|
||||
WHEN "0" THEN r.`originalname`
|
||||
ELSE GROUP_CONCAT(m.`value` ORDER BY ms.`thumbtitle`, ms.`sorter` SEPARATOR " - ")
|
||||
END AS title
|
||||
FROM metadatas AS m FORCE INDEX(`record_id`)
|
||||
STRAIGHT_JOIN metadatas_structure AS ms ON (ms.`id` = m.`meta_struct_id`)
|
||||
STRAIGHT_JOIN record AS r ON (r.`record_id` = m.`record_id`)
|
||||
WHERE m.`record_id` IN (?)
|
||||
GROUP BY m.`record_id`, ms.`thumbtitle`
|
||||
SQL;
|
||||
$sql = "SELECT\n"
|
||||
. "m.`record_id`,\n"
|
||||
. " CASE ms.`thumbtitle`\n"
|
||||
. " WHEN '1' THEN 'default'\n"
|
||||
. " WHEN '0' THEN 'default'\n"
|
||||
. " ELSE ms.`thumbtitle`\n"
|
||||
. " END AS locale,\n"
|
||||
. " CASE ms.`thumbtitle`\n"
|
||||
. " WHEN '0' THEN r.`originalname`\n"
|
||||
. " ELSE GROUP_CONCAT(m.`value` ORDER BY ms.`thumbtitle`, ms.`sorter` SEPARATOR ' - ')\n"
|
||||
. " END AS title\n"
|
||||
. "FROM metadatas AS m FORCE INDEX(`record_id`)\n"
|
||||
. "STRAIGHT_JOIN metadatas_structure AS ms ON (ms.`id` = m.`meta_struct_id`)\n"
|
||||
. "STRAIGHT_JOIN record AS r ON (r.`record_id` = m.`record_id`)\n"
|
||||
. "WHERE m.`record_id` IN (?)\n"
|
||||
. "GROUP BY m.`record_id`, ms.`thumbtitle`\n";
|
||||
|
||||
$statement = $this->connection->executeQuery(
|
||||
$sql,
|
||||
array(array_keys($records)),
|
||||
@@ -50,7 +55,7 @@ SQL;
|
||||
);
|
||||
|
||||
while ($row = $statement->fetch()) {
|
||||
$records[$row['record_id']]['title'][$row['locale']] = $row['title'];
|
||||
$records[$row['record_id']]['title'][$row['locale']] = $this->helper->sanitizeValue($row['title'], FieldMapping::TYPE_STRING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -57,6 +57,9 @@ class DateFieldMapping extends ComplexFieldMapping
|
||||
*/
|
||||
protected function getProperties()
|
||||
{
|
||||
return array_merge([ 'format' => $this->format ], parent::getProperties());
|
||||
return array_merge([
|
||||
'format' => $this->format,
|
||||
'ignore_malformed' => true
|
||||
], parent::getProperties());
|
||||
}
|
||||
}
|
||||
|
@@ -89,31 +89,72 @@ class RecordHelper
|
||||
return $this->collectionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $date
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateDate($date)
|
||||
{
|
||||
$d = DateTime::createFromFormat(FieldMapping::DATE_FORMAT_CAPTION_PHP, $date);
|
||||
|
||||
return $d && $d->format(FieldMapping::DATE_FORMAT_CAPTION_PHP) == $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return null|string
|
||||
*/
|
||||
public static function sanitizeDate($value)
|
||||
{
|
||||
// introduced in https://github.com/alchemy-fr/Phraseanet/commit/775ce804e0257d3a06e4e068bd17330a79eb8370#diff-bee690ed259e0cf73a31dee5295d2edcR286
|
||||
// not sure if it's really needed
|
||||
$v_fix = null;
|
||||
try {
|
||||
$date = new \DateTime($value);
|
||||
|
||||
return $date->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
|
||||
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
|
||||
switch (count($a)) {
|
||||
case 1: // yyyy
|
||||
$date = new \DateTime($a[0] . '-01-01'); // will throw if date is not valid
|
||||
$v_fix = $date->format('Y');
|
||||
break;
|
||||
case 2: // yyyy;mm
|
||||
$date = new \DateTime( $a[0] . '-' . $a[1] . '-01');
|
||||
$v_fix = $date->format('Y-m');
|
||||
break;
|
||||
case 3: // yyyy;mm;dd
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2]);
|
||||
$v_fix = $date->format('Y-m-d');
|
||||
break;
|
||||
case 4:
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':00:00');
|
||||
$v_fix = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
case 5:
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':00');
|
||||
$v_fix = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
case 6:
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]);
|
||||
$v_fix = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
// no-op, v_fix = null
|
||||
}
|
||||
|
||||
return $v_fix;
|
||||
}
|
||||
|
||||
public function sanitizeValue($value, $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case FieldMapping::TYPE_DATE:
|
||||
return self::sanitizeDate($value);
|
||||
|
||||
case FieldMapping::TYPE_FLOAT:
|
||||
case FieldMapping::TYPE_DOUBLE:
|
||||
return (float) $value;
|
||||
|
||||
case FieldMapping::TYPE_INTEGER:
|
||||
case FieldMapping::TYPE_LONG:
|
||||
case FieldMapping::TYPE_SHORT:
|
||||
case FieldMapping::TYPE_BYTE:
|
||||
return (int) $value;
|
||||
|
||||
case FieldMapping::TYPE_BOOLEAN:
|
||||
return (bool) $value;
|
||||
|
||||
case FieldMapping::TYPE_STRING:
|
||||
return str_replace("\0", '', $value);
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -110,41 +110,50 @@ class QueryHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static function getRangeFromDateString($string)
|
||||
public static function getRangeFromDateString($value)
|
||||
{
|
||||
$formats = ['Y/m/d', 'Y/m', 'Y'];
|
||||
$deltas = ['+1 day', '+1 month', '+1 year'];
|
||||
$to = null;
|
||||
while ($format = array_pop($formats)) {
|
||||
$delta = array_pop($deltas);
|
||||
$from = date_create_from_format($format, $string);
|
||||
if ($from !== false) {
|
||||
// Rewind to start of range
|
||||
$month = 1;
|
||||
$day = 1;
|
||||
switch ($format) {
|
||||
case 'Y/m/d':
|
||||
$day = (int) $from->format('d');
|
||||
case 'Y/m':
|
||||
$month = (int) $from->format('m');
|
||||
case 'Y':
|
||||
$year = (int) $from->format('Y');
|
||||
}
|
||||
date_date_set($from, $year, $month, $day);
|
||||
date_time_set($from, 0, 0, 0);
|
||||
// Create end of the the range
|
||||
$to = date_modify(clone $from, $delta);
|
||||
break;
|
||||
$date_from = null;
|
||||
$date_to = null;
|
||||
try {
|
||||
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
|
||||
switch (count($a)) {
|
||||
case 1: // yyyy
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-01-01 00:00:00')); // will throw if date is not valid
|
||||
$date_to->add(new \DateInterval('P1Y'));
|
||||
break;
|
||||
case 2: // yyyy;mm
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-01 00:00:00')); // will throw if date is not valid
|
||||
$date_to->add(new \DateInterval('P1M'));
|
||||
break;
|
||||
case 3: // yyyy;mm;dd
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' 00:00:00')); // will throw if date is not valid
|
||||
$date_to->add(new \DateInterval('P1D'));
|
||||
break;
|
||||
case 4:
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':00:00'));
|
||||
$date_to->add(new \DateInterval('PT1H'));
|
||||
break;
|
||||
case 5:
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':00'));
|
||||
$date_to->add(new \DateInterval('PT1M'));
|
||||
break;
|
||||
case 6:
|
||||
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]));
|
||||
// $date_to->add(new \DateInterval('PT1S')); // no need since precision is 1 sec, a "equal" will be generated when from==to
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
if (!$from || !$to) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid date "%s".', $string));
|
||||
if ($date_from === null || $date_to === null) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid date "%s".', $value));
|
||||
}
|
||||
|
||||
return [
|
||||
'from' => $from->format(FieldMapping::DATE_FORMAT_CAPTION_PHP),
|
||||
'to' => $to->format(FieldMapping::DATE_FORMAT_CAPTION_PHP)
|
||||
'from' => $date_from->format('Y-m-d H:i:s'),
|
||||
'to' => $date_to->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\Search;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\Exception;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Structure;
|
||||
use Hoa\Compiler\Llk\TreeNode;
|
||||
use Hoa\Visitor\Element;
|
||||
@@ -166,6 +166,12 @@ class QueryVisitor implements Visit
|
||||
$key = $node->getChild(0)->accept($this);
|
||||
$boundary = $node->getChild(1)->accept($this);
|
||||
|
||||
if ($this->isDateKey($key)) {
|
||||
if(($v = RecordHelper::sanitizeDate($boundary)) !== null) {
|
||||
$boundary = $v;
|
||||
}
|
||||
}
|
||||
|
||||
switch ($node->getId()) {
|
||||
case NodeTypes::LT_EXPR:
|
||||
return AST\KeyValue\RangeExpression::lessThan($key, $boundary);
|
||||
@@ -195,11 +201,15 @@ class QueryVisitor implements Visit
|
||||
try {
|
||||
// Try to create a range for incomplete dates
|
||||
$range = QueryHelper::getRangeFromDateString($right);
|
||||
return new AST\KeyValue\RangeExpression(
|
||||
$left,
|
||||
$range['from'], true,
|
||||
$range['to'], false
|
||||
);
|
||||
if ($range['from'] === $range['to']) {
|
||||
return new AST\KeyValue\EqualExpression($left, $range['from']);
|
||||
} else {
|
||||
return new AST\KeyValue\RangeExpression(
|
||||
$left,
|
||||
$range['from'], true,
|
||||
$range['to'], false
|
||||
);
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Fall back to equal expression
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Assert\Assertion;
|
||||
|
||||
@@ -20,7 +19,7 @@ class ValueChecker
|
||||
{
|
||||
Assertion::allIsInstanceOf($list, Typed::class);
|
||||
$is_numeric = is_numeric($value);
|
||||
$is_valid_date = RecordHelper::validateDate($value);
|
||||
$is_valid_date = (RecordHelper::sanitizeDate($value) !== null);
|
||||
$filtered = [];
|
||||
foreach ($list as $item) {
|
||||
switch ($item->getType()) {
|
||||
|
@@ -121,13 +121,16 @@ class WriteMetadataJob extends AbstractJob
|
||||
$fieldName = $fieldStructure->get_name();
|
||||
|
||||
// skip fields with no src
|
||||
if($tagName == '') {
|
||||
if($tagName == '' || $tagName == 'Phraseanet:no-source') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check exiftool known tags to skip Phraseanet:tf-*
|
||||
try {
|
||||
TagFactory::getFromRDFTagname($tagName);
|
||||
$tag = TagFactory::getFromRDFTagname($tagName);
|
||||
if(!$tag->isWritable()) {
|
||||
continue;
|
||||
}
|
||||
} catch (TagUnknown $e) {
|
||||
continue;
|
||||
}
|
||||
@@ -139,29 +142,42 @@ class WriteMetadataJob extends AbstractJob
|
||||
if ($fieldStructure->is_multi()) {
|
||||
$values = array();
|
||||
foreach ($fieldValues as $value) {
|
||||
$values[] = $value->getValue();
|
||||
$values[] = $this->removeNulChar($value->getValue());
|
||||
}
|
||||
|
||||
$value = new Value\Multi($values);
|
||||
} else {
|
||||
$fieldValue = array_pop($fieldValues);
|
||||
$value = $fieldValue->getValue();
|
||||
$value = $this->removeNulChar($fieldValue->getValue());
|
||||
|
||||
$value = new Value\Mono($value);
|
||||
// fix the dates edited into phraseanet
|
||||
if($fieldStructure->get_type() === $fieldStructure::TYPE_DATE) {
|
||||
try {
|
||||
$value = self::fixDate($value); // will return NULL if the date is not valid
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$value = null; // do NOT write back to iptc
|
||||
}
|
||||
}
|
||||
|
||||
if($value !== null) { // do not write invalid dates
|
||||
$value = new Value\Mono($value);
|
||||
}
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
// the field is not set in the record, erase it
|
||||
if ($fieldStructure->is_multi()) {
|
||||
$value = new Value\Multi(array(''));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$value = new Value\Mono('');
|
||||
}
|
||||
}
|
||||
|
||||
$metadata->add(
|
||||
new Metadata\Metadata($fieldStructure->get_tag(), $value)
|
||||
);
|
||||
if($value !== null) { // do not write invalid data
|
||||
$metadata->add(
|
||||
new Metadata\Metadata($fieldStructure->get_tag(), $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$writer = $this->getMetadataWriter($jobData->getApplication());
|
||||
@@ -215,4 +231,39 @@ class WriteMetadataJob extends AbstractJob
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function removeNulChar($value)
|
||||
{
|
||||
return str_replace("\0", "", $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* re-format a phraseanet date for iptc writing
|
||||
* return NULL if the date is not valid
|
||||
*
|
||||
* @param string $value
|
||||
* @return string|null
|
||||
*/
|
||||
private static function fixDate($value)
|
||||
{
|
||||
$date = null;
|
||||
try {
|
||||
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
|
||||
switch (count($a)) {
|
||||
case 3: // yyyy;mm;dd
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2]);
|
||||
$date = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
case 6: // yyyy;mm;dd;hh;mm;ss
|
||||
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]);
|
||||
$date = $date->format('Y-m-d H:i:s');
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$date = null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
@@ -1115,7 +1115,6 @@ class ACL implements cache_cacheableInterface
|
||||
/**
|
||||
* @param array $base_ids
|
||||
* @return $this
|
||||
* @throws DBALException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function revoke_access_from_bases(Array $base_ids)
|
||||
@@ -1125,24 +1124,30 @@ class ACL implements cache_cacheableInterface
|
||||
|
||||
$usr_id = $this->user->getId();
|
||||
|
||||
$errors = 0;
|
||||
foreach ($base_ids as $base_id) {
|
||||
if (!$stmt_del->execute([':base_id' => $base_id, ':usr_id' => $usr_id])) {
|
||||
throw new Exception('Error while deleteing some rights');
|
||||
}
|
||||
|
||||
$this->app['dispatcher']->dispatch(
|
||||
AclEvents::ACCESS_TO_BASE_REVOKED,
|
||||
new AccessToBaseRevokedEvent(
|
||||
$this,
|
||||
array(
|
||||
'base_id'=>$base_id
|
||||
if ($stmt_del->execute([':base_id' => $base_id, ':usr_id' => $usr_id])) {
|
||||
$this->app['dispatcher']->dispatch(
|
||||
AclEvents::ACCESS_TO_BASE_REVOKED,
|
||||
new AccessToBaseRevokedEvent(
|
||||
$this,
|
||||
[
|
||||
'base_id' => $base_id
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
else {
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
$stmt_del->closeCursor();
|
||||
$this->delete_data_from_cache(self::CACHE_RIGHTS_BAS);
|
||||
|
||||
if($errors > 0) {
|
||||
throw new Exception('Error while deleting some rights');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
4
lib/classes/cache/databox.php
vendored
@@ -121,9 +121,9 @@ class cache_databox
|
||||
|
||||
$conn = $app->getApplicationBox()->get_connection();
|
||||
|
||||
$sql = 'UPDATE sitepreff SET memcached_update = :date';
|
||||
$sql = 'UPDATE sitepreff SET memcached_update = current_timestamp()';
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->execute([':date' => $now]);
|
||||
$stmt->execute();
|
||||
$stmt->closeCursor();
|
||||
|
||||
self::$refreshing = false;
|
||||
|
@@ -11,6 +11,9 @@
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Model\Entities\User;
|
||||
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\UserRepository;
|
||||
|
||||
|
||||
class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
|
||||
{
|
||||
@@ -31,9 +34,9 @@ class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Array $datas
|
||||
* @param string[] $data
|
||||
* @param boolean $unread
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function datas(array $data, $unread)
|
||||
{
|
||||
@@ -41,24 +44,29 @@ class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
|
||||
$ssel_id = $data['ssel_id'];
|
||||
$n = $data['n'];
|
||||
|
||||
if (null === $user= $this->app['repo.users']->find(($from))) {
|
||||
/** @var UserRepository $userRepo */
|
||||
$userRepo = $this->app['repo.users'];
|
||||
if( ($user= $userRepo->find(($from))) === null ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sender = $user->getDisplayName();
|
||||
|
||||
try {
|
||||
/** @var BasketRepository $repository */
|
||||
$repository = $this->app['repo.baskets'];
|
||||
|
||||
$basket = $repository->findUserBasket($ssel_id, $this->app->getAuthenticatedUser(), false);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'text' => $this->app->trans('%user% vous a delivre %quantity% document(s) pour votre commande %title%', ['%user%' => $sender, '%quantity%' => $n, '%title%' => '<a href="/lightbox/compare/'
|
||||
. $ssel_id . '/" target="_blank">'
|
||||
. $basket->getName() . '</a>'])
|
||||
, 'class' => ''
|
||||
. $basket->getName() . '</a>']),
|
||||
'class' => ''
|
||||
];
|
||||
|
||||
return $ret;
|
||||
|
@@ -11,6 +11,9 @@
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Model\Entities\User;
|
||||
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\UserRepository;
|
||||
|
||||
|
||||
class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
|
||||
{
|
||||
@@ -31,35 +34,38 @@ class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $datas
|
||||
* @param string[] $data
|
||||
* @param boolean $unread
|
||||
* @return Array
|
||||
* @return array
|
||||
*/
|
||||
public function datas(array $data, $unread)
|
||||
{
|
||||
$from = $data['from'];
|
||||
$ssel_id = $data['ssel_id'];
|
||||
|
||||
if (null === $registered_user = $this->app['repo.users']->find($from)) {
|
||||
/** @var UserRepository $userRepo */
|
||||
$userRepo = $this->app['repo.users'];
|
||||
if ( ($registered_user = $userRepo->find($from)) === null ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sender = $registered_user->getDisplayName();
|
||||
|
||||
try {
|
||||
/** @var BasketRepository $repository */
|
||||
$repository = $this->app['repo.baskets'];
|
||||
|
||||
$basket = $repository->findUserBasket($ssel_id, $this->app->getAuthenticatedUser(), false);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'text' => $this->app->trans('%user% a envoye son rapport de validation de %title%', ['%user%' => $sender, '%title%' => '<a href="/lightbox/validate/'
|
||||
. $ssel_id . '/" target="_blank">'
|
||||
. $basket->getName() . '</a>'
|
||||
])
|
||||
, 'class' => ''
|
||||
. $basket->getName() . '</a>']),
|
||||
'class' => ''
|
||||
];
|
||||
|
||||
return $ret;
|
||||
@@ -84,12 +90,18 @@ class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
|
||||
}
|
||||
|
||||
/**
|
||||
* @param integer $usr_id The id of the user to check
|
||||
* @param User $user The id of the user to check
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available(User $user)
|
||||
{
|
||||
return $this->app->getAclForUser($user)->has_right(\ACL::CANPUSH);
|
||||
try {
|
||||
return $this->app->getAclForUser($user)->has_right(\ACL::CANPUSH);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// has_right(unknow_right) ? will not happen !
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,13 +9,16 @@
|
||||
*/
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
|
||||
use Alchemy\Phrasea\Model\Entities\Token;
|
||||
use Alchemy\Phrasea\Model\Entities\User;
|
||||
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
|
||||
use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
|
||||
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
|
||||
use Assert\Assertion;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
|
||||
class set_export extends set_abstract
|
||||
{
|
||||
private static $maxFilenameLength = 256;
|
||||
@@ -60,6 +63,7 @@ class set_export extends set_abstract
|
||||
$remain_hd = [];
|
||||
|
||||
if ($storyWZid) {
|
||||
/** @var StoryWZRepository $repository */
|
||||
$repository = $app['repo.story-wz'];
|
||||
|
||||
$storyWZ = $repository->findByUserAndId($this->app, $app->getAuthenticatedUser(), $storyWZid);
|
||||
@@ -68,6 +72,7 @@ class set_export extends set_abstract
|
||||
}
|
||||
|
||||
if ($sstid != "") {
|
||||
/** @var BasketRepository $repository */
|
||||
$repository = $app['repo.baskets'];
|
||||
|
||||
$Basket = $repository->findUserBasket($sstid, $app->getAuthenticatedUser(), false);
|
||||
|
@@ -1278,6 +1278,27 @@
|
||||
<field>id</field>
|
||||
</fields>
|
||||
</index>
|
||||
<index>
|
||||
<name>usr_id</name>
|
||||
<type>INDEX</type>
|
||||
<fields>
|
||||
<field>usr_id</field>
|
||||
</fields>
|
||||
</index>
|
||||
<index>
|
||||
<name>unread</name>
|
||||
<type>INDEX</type>
|
||||
<fields>
|
||||
<field>unread</field>
|
||||
</fields>
|
||||
</index>
|
||||
<index>
|
||||
<name>created_on</name>
|
||||
<type>INDEX</type>
|
||||
<fields>
|
||||
<field>created_on</field>
|
||||
</fields>
|
||||
</index>
|
||||
</indexes>
|
||||
<engine>InnoDB</engine>
|
||||
</table>
|
||||
|
@@ -234,6 +234,7 @@ embed_bundle:
|
||||
audio:
|
||||
player: videojs
|
||||
autoplay: false
|
||||
cover_subdef: thumbnail
|
||||
document:
|
||||
#player: flexpaper
|
||||
enable_pdfjs: true
|
||||
|
@@ -65,7 +65,7 @@
|
||||
"normalize-css": "^2.1.0",
|
||||
"npm": "^6.0.0",
|
||||
"npm-modernizr": "^2.8.3",
|
||||
"phraseanet-production-client": "^0.34.16-d",
|
||||
"phraseanet-production-client": "0.34.72-d",
|
||||
"requirejs": "^2.3.5",
|
||||
"tinymce": "^4.0.28",
|
||||
"underscore": "^1.8.3",
|
||||
|
21
resources/ansible/playbook-boxes.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
- hosts: all
|
||||
sudo: true
|
||||
vars_files:
|
||||
- vars/all.yml
|
||||
roles:
|
||||
# - server
|
||||
# - repositories
|
||||
# - vagrant_local
|
||||
- nginx
|
||||
# - mariadb
|
||||
# - elasticsearch
|
||||
# - rabbitmq
|
||||
# - php
|
||||
- xdebug
|
||||
# - composer
|
||||
- mailcatcher
|
||||
# - node
|
||||
# - yarn
|
||||
# - ffmpeg
|
||||
- app
|
@@ -4,18 +4,18 @@
|
||||
vars_files:
|
||||
- vars/all.yml
|
||||
roles:
|
||||
# - server
|
||||
# - repositories
|
||||
# - vagrant_local
|
||||
- server
|
||||
- repositories
|
||||
- vagrant_local
|
||||
- nginx
|
||||
# - mariadb
|
||||
# - elasticsearch
|
||||
# - rabbitmq
|
||||
# - php
|
||||
- mariadb
|
||||
- elasticsearch
|
||||
- rabbitmq
|
||||
- php
|
||||
- xdebug
|
||||
# - composer
|
||||
# - mailcatcher
|
||||
# - node
|
||||
# - yarn
|
||||
# - ffmpeg
|
||||
- composer
|
||||
- mailcatcher
|
||||
- node
|
||||
- yarn
|
||||
- ffmpeg
|
||||
- app
|
||||
|
@@ -13,7 +13,7 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Install Dependencies
|
||||
apt: pkg=openjdk-7-jre state=latest
|
||||
apt: pkg=openjdk-8-jre state=latest
|
||||
|
||||
- name: Remove temporary debian package
|
||||
shell: rm -f /tmp/elasticsearch-{{ elasticsearch.version }}.deb
|
||||
|
@@ -13,7 +13,7 @@
|
||||
|
||||
- name: Install mailcatcher gem
|
||||
# gem module is flaky, this is consistent
|
||||
command: gem install mailcatcher --conservative
|
||||
command: gem install mailcatcher -v 0.6.4 --conservative
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Install mailcatcher supervisord conf
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
- name: Add Key for MariaDB Repository
|
||||
sudo: yes
|
||||
apt_key: url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xcbcb082a1bb943db
|
||||
apt_key: url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF1656F24C74CD1D8
|
||||
|
||||
# RabbitMQ
|
||||
- name: Add rabbitmq package repository
|
||||
|
@@ -34,12 +34,14 @@ server:
|
||||
- fr_FR.UTF-8
|
||||
- de_DE.UTF-8
|
||||
- nl_NL.UTF-8
|
||||
|
||||
repositories:
|
||||
php: 'ppa:ondrej/php'
|
||||
mariadb: 'deb http://mirror6.layerjet.com/mariadb/repo/10.1/ubuntu'
|
||||
mariadb: 'deb [arch=amd64,arm64,i386,ppc64el] http://mirror.nodesdirect.com/mariadb/repo/10.3/ubuntu'
|
||||
elasticsearch: 'ppa:webupd8team/java'
|
||||
rabbitmq: 'deb http://www.rabbitmq.com/debian/ testing main'
|
||||
yarn: 'https://dl.yarnpkg.com/debian/'
|
||||
|
||||
vagrant_local:
|
||||
install: '1'
|
||||
vm:
|
||||
|
@@ -148,6 +148,9 @@ $mainMenuLinkBackgroundHoverColor: transparent;
|
||||
}
|
||||
#FNDR a {
|
||||
text-decoration: none;
|
||||
img {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
#FNDR a:hover {
|
||||
text-decoration: none;
|
||||
|
BIN
resources/www/common/images/Thumbs.db
Normal file
BIN
resources/www/common/images/blank.gif
Normal file
After Width: | Height: | Size: 49 B |
BIN
resources/www/common/images/colorpicker_background.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
resources/www/common/images/colorpicker_hex.png
Normal file
After Width: | Height: | Size: 532 B |
BIN
resources/www/common/images/colorpicker_hsb_b.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
resources/www/common/images/colorpicker_hsb_h.png
Normal file
After Width: | Height: | Size: 1012 B |
BIN
resources/www/common/images/colorpicker_hsb_s.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/www/common/images/colorpicker_indic.gif
Normal file
After Width: | Height: | Size: 86 B |
BIN
resources/www/common/images/colorpicker_overlay.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
resources/www/common/images/colorpicker_rgb_b.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
resources/www/common/images/colorpicker_rgb_g.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/www/common/images/colorpicker_rgb_r.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/www/common/images/colorpicker_select.gif
Normal file
After Width: | Height: | Size: 78 B |
BIN
resources/www/common/images/colorpicker_submit.png
Normal file
After Width: | Height: | Size: 984 B |
BIN
resources/www/common/images/custom_background.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/www/common/images/custom_hex.png
Normal file
After Width: | Height: | Size: 562 B |
BIN
resources/www/common/images/custom_hsb_b.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/www/common/images/custom_hsb_h.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
resources/www/common/images/custom_hsb_s.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/www/common/images/custom_indic.gif
Normal file
After Width: | Height: | Size: 86 B |
BIN
resources/www/common/images/custom_rgb_b.png
Normal file
After Width: | Height: | Size: 1008 B |
BIN
resources/www/common/images/custom_rgb_g.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
resources/www/common/images/custom_rgb_r.png
Normal file
After Width: | Height: | Size: 1018 B |
BIN
resources/www/common/images/custom_submit.png
Normal file
After Width: | Height: | Size: 997 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1010 B After Width: | Height: | Size: 1.3 KiB |
BIN
resources/www/common/images/select.png
Normal file
After Width: | Height: | Size: 506 B |
BIN
resources/www/common/images/select2.png
Normal file
After Width: | Height: | Size: 518 B |
BIN
resources/www/common/images/slider.png
Normal file
After Width: | Height: | Size: 315 B |
@@ -31,17 +31,13 @@ var commonModule = (function ($, p4) {
|
||||
$(this).removeClass('context-menu-item-hover');
|
||||
});
|
||||
|
||||
// $('#help-trigger').contextMenu('#mainMenu .helpcontextmenu', {openEvt: 'click', dropDown: true, theme: 'vista', dropDown: true,
|
||||
// showTransition: 'slideDown',
|
||||
// hideTransition: 'hide',
|
||||
// shadow: false
|
||||
// });
|
||||
|
||||
$('body').on('click', '.infoDialog', function (event) {
|
||||
infoDialog($(this));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function showOverlay(n, appendto, callback, zIndex) {
|
||||
|
||||
var div = "OVERLAY";
|
||||
|
167
resources/www/common/styles/colorpicker.scss
Normal file
@@ -0,0 +1,167 @@
|
||||
$colorPickerImagesPath: '/assets/common/images/' !default;
|
||||
|
||||
.colorpicker {
|
||||
width: 356px;
|
||||
height: 176px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_background.png');
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
display: none;
|
||||
}
|
||||
.colorpicker_color {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
left: 14px;
|
||||
top: 13px;
|
||||
position: absolute;
|
||||
background: #f00;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.colorpicker_color div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_overlay.png');
|
||||
}
|
||||
.colorpicker_color div div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
overflow: hidden;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_select.gif');
|
||||
margin: -5px 0 0 -5px;
|
||||
}
|
||||
.colorpicker_hue {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 171px;
|
||||
width: 35px;
|
||||
height: 150px;
|
||||
cursor: n-resize;
|
||||
}
|
||||
.colorpicker_hue div {
|
||||
position: absolute;
|
||||
width: 35px;
|
||||
height: 9px;
|
||||
overflow: hidden;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_indic.gif') left top;
|
||||
margin: -4px 0 0 0;
|
||||
left: 0px;
|
||||
}
|
||||
.colorpicker_new_color {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
left: 213px;
|
||||
top: 13px;
|
||||
background: #f00;
|
||||
}
|
||||
.colorpicker_current_color {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
left: 283px;
|
||||
top: 13px;
|
||||
background: #f00;
|
||||
}
|
||||
.colorpicker input {
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #898989;
|
||||
top: 4px;
|
||||
right: 11px;
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 11px;
|
||||
}
|
||||
.colorpicker_hex {
|
||||
position: absolute;
|
||||
width: 72px;
|
||||
height: 22px;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_hex.png') top;
|
||||
left: 212px;
|
||||
top: 142px;
|
||||
}
|
||||
.colorpicker_hex input {
|
||||
right: 6px;
|
||||
}
|
||||
.colorpicker_field {
|
||||
height: 22px;
|
||||
width: 62px;
|
||||
background-position: top;
|
||||
position: absolute;
|
||||
}
|
||||
.colorpicker_field span {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 22px;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: n-resize;
|
||||
}
|
||||
.colorpicker_rgb_r {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_r.png');
|
||||
top: 52px;
|
||||
left: 212px;
|
||||
}
|
||||
.colorpicker_rgb_g {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_g.png');
|
||||
top: 82px;
|
||||
left: 212px;
|
||||
}
|
||||
.colorpicker_rgb_b {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_b.png');
|
||||
top: 112px;
|
||||
left: 212px;
|
||||
}
|
||||
.colorpicker_hsb_h {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_h.png');
|
||||
top: 52px;
|
||||
left: 282px;
|
||||
}
|
||||
.colorpicker_hsb_s {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_s.png');
|
||||
top: 82px;
|
||||
left: 282px;
|
||||
}
|
||||
.colorpicker_hsb_b {
|
||||
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_b.png');
|
||||
top: 112px;
|
||||
left: 282px;
|
||||
}
|
||||
.colorpicker_submit {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: url('#{$colorPickerImagesPath}colorpicker_submit.png') top;
|
||||
left: 322px;
|
||||
top: 142px;
|
||||
overflow: hidden;
|
||||
.submiter {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.colorpicker_focus {
|
||||
background-position: center;
|
||||
}
|
||||
.colorpicker_hex.colorpicker_focus {
|
||||
background-position: bottom;
|
||||
}
|
||||
.colorpicker_submit.colorpicker_focus {
|
||||
background-position: bottom;
|
||||
}
|
||||
.colorpicker_slider {
|
||||
background-position: bottom;
|
||||
}
|
||||
|