Skip to content

Commit

Permalink
Merge pull request #56 from wikimedia/uploading-T210564
Browse files Browse the repository at this point in the history
Upload translated SVG file to remote wiki
  • Loading branch information
MaxSem committed Jan 29, 2019
2 parents 78a1abf + 3244967 commit 348c087
Show file tree
Hide file tree
Showing 20 changed files with 259 additions and 11 deletions.
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ project.

## Install for development

All methods of installation require the registration of an OAuth consumer
on the wiki to which you will upload SVGs (probably a local development wiki).
Do this via e.g. `http://localhost/wiki/Special:OAuthConsumerRegistration/propose`
and make sure you include the following grants:

* Create, edit, and move pages
* Upload new files
* Upload, replace, and move files

The `OAUTH_URL` environment variable must be set to the long form of the URL
such as `http://localhost/w/index.php?title=Special:OAuth`.

To configure the wiki that's used to fetch images and for upload
you should also set `WIKI_URL` environment variable
to the API URL of your development wiki, e.g. `http://localhost/w/api.php`.

### Install using Docker

Prerequisites:
Expand Down
3 changes: 2 additions & 1 deletion assets/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ $( function () {
return;
}
function switchToNewTargetLang( ulsElement, language ) {
// 1. Save the language name and code in the widget.
// 1. Save the language name and code in the widget and the hidden form field.
ulsElement.setLabel( $.uls.data.languages[ language ][ 2 ] );
ulsElement.setData( language );
ulsElement.setValue( language );
$( "input[name='target-lang']" ).val( language );

// 2. Switch what's displayed in the form when a new language is selected in the ULS.
$( '.translation-fields .oo-ui-fieldLayout' ).each( function () {
Expand Down
22 changes: 22 additions & 0 deletions assets/translate.less
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ body.translate {

main {

aside.upload-complete {
max-width: 950px;
margin: auto;
background-color: @wmui-color-green90;
border: 1px solid @wmui-color-green30;
border-radius: 3px;
padding: 1em;
h3 {
margin: 0;
font-family: @font-family-system-sans;
}
.buttons a {
padding: 0.7em;
background-color: rgba(0, 0, 0, 0.1);
color: @wmui-color-base0;
border-radius: 3px;
text-transform: uppercase;
margin-right: 1em;
font-size: smaller;
}
}

form {
padding: 2rem 5rem;
display: flex;
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"require": {
"php": "^7.2",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-json": "*",
Expand Down
3 changes: 2 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ services:
App\Service\MediaWikiApi:
arguments:
$entryPoint: '%env(WIKI_URL)%'
$client: '@MediaWiki\OAuthClient\Client'
$session: "@session"

App\Service\FileCache:
arguments:
Expand Down
5 changes: 5 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"update-commons-title": "Update Commons",
"update-commons-desc": "or download directly to your computer.",

"upload-complete": "Upload complete",
"upload-complete-message": "Thanks! Your translations to $1 have been uploaded. You may continue translating the labels and upload again to update the image.",
"upload-complete-commons": "View image",
"upload-complete-translate-another": "Translate another image",

"licenced-under": "Licenced under GPL 3.0 or later",
"developed-by": "Developed by the Wikimedia Foundation's Community Tech team",
"toolforge-logo-alt": "Toolforge 'anvil' logo",
Expand Down
5 changes: 5 additions & 0 deletions i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"update-commons-title": "The title of the 'update commons' step of the tutorial box.",
"update-commons-desc": "The short description of the 'update commons' step of the tutorial box. May be a continuation of the phrase in {{msg-mw|update-commons-title}}.",

"upload-complete": "Header text for the banner shown after successful upload of a file.",
"upload-complete-message": "Text in the banner shown after successful upload of a file.\n$1 is the filename of the image (without link).",
"upload-complete-commons": "First of two buttons shown in the successful-upload banner, linking to the newly-uploaded file on Commons.",
"upload-complete-translate-another": "Second of two buttons shown in the successful-upload banner, linking back to the SVG Translate search form.",

"licenced-under": "Sentence describing the licence of the application, shown as a link. Displayed in the footer on every page.",
"developed-by": "By-line for the application, shown as a link to CommTech's wiki page. Displayed in the footer on every page.",
"toolforge-logo-alt": "Alt text for the Toolforge anvil image.",
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
<env name="MAILER_URL" value="null://localhost"/>
<!-- ###- symfony/swiftmailer-bundle ### -->

<env name="OAUTH_KEY" value="" />
<env name="OAUTH_SECRET" value="" />
<env name="OAUTH_URL" value="https://meta.wikimedia.org/w/index.php?title=Special:OAuth" />
<env name="WIKI_URL" value="https://commons.wikimedia.org/w/api.php" />

</php>
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/assets/entrypoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"entrypoints": {
"app": {
"css": [
"assets/app.771d641e.css"
"assets/app.cdb1250c.css"
],
"js": [
"assets/app.a1bba804.js"
"assets/app.7fd078db.js"
]
}
}
Expand Down
5 changes: 5 additions & 0 deletions public/assets/i18n/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"update-commons-title": "Update Commons",
"update-commons-desc": "or download directly to your computer.",

"upload-complete": "Upload complete",
"upload-complete-message": "Thanks! Your translations to $1 have been uploaded. You may continue translating the labels and upload again to update the image.",
"upload-complete-commons": "View image",
"upload-complete-translate-another": "Translate another image",

"licenced-under": "Licenced under GPL 3.0 or later",
"developed-by": "Developed by the Wikimedia Foundation's Community Tech team",
"toolforge-logo-alt": "Toolforge 'anvil' logo",
Expand Down
5 changes: 5 additions & 0 deletions public/assets/i18n/app/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"update-commons-title": "The title of the 'update commons' step of the tutorial box.",
"update-commons-desc": "The short description of the 'update commons' step of the tutorial box. May be a continuation of the phrase in {{msg-mw|update-commons-title}}.",

"upload-complete": "Header text for the banner shown after successful upload of a file.",
"upload-complete-message": "Text in the banner shown after successful upload of a file.\n$1 is the filename of the image (without link).",
"upload-complete-commons": "First of two buttons shown in the successful-upload banner, linking to the newly-uploaded file on Commons.",
"upload-complete-translate-another": "Second of two buttons shown in the successful-upload banner, linking back to the SVG Translate search form.",

"licenced-under": "Sentence describing the licence of the application, shown as a link. Displayed in the footer on every page.",
"developed-by": "By-line for the application, shown as a link to CommTech's wiki page. Displayed in the footer on every page.",
"toolforge-logo-alt": "Alt text for the Toolforge anvil image.",
Expand Down
4 changes: 2 additions & 2 deletions public/assets/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"assets/app.css": "assets/app.771d641e.css",
"assets/app.js": "assets/app.a1bba804.js",
"assets/app.css": "assets/app.cdb1250c.css",
"assets/app.js": "assets/app.7fd078db.js",
"assets/grabbing.cur": "assets/a8c874b93b3d848f39a71260c57e3863.cur",
"assets/grab.cur": "assets/b06c243f534d9c5461d16528156cd5a8.cur",
"assets/i18n/app/af.json": "assets/i18n/app/af.json",
Expand Down
53 changes: 52 additions & 1 deletion src/Controller/TranslateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Model\Svg\SvgFile;
use App\Model\Title;
use App\Service\FileCache;
use App\Service\Uploader;
use Krinkle\Intuition\Intuition;
use OOUI\ButtonInputWidget;
use OOUI\DropdownInputWidget;
Expand All @@ -14,8 +15,10 @@
use OOUI\LabelWidget;
use OOUI\TextInputWidget;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;

Expand Down Expand Up @@ -65,6 +68,7 @@ public function translate(
'flags' => [ 'progressive' ],
'type' => 'submit',
'icon' => 'logoWikimediaCommons',
'name' => 'upload',
]);
if (!$session->get('logged_in_user')) {
// Only logged in users can upload.
Expand Down Expand Up @@ -106,6 +110,7 @@ public function translate(
'classes' => ['target-lang-widget'],
'indicator' => 'down',
'infusable' => true,
'name' => 'target-lang',
]);
$languageSelectorsLayout = new HorizontalLayout([
'items' => [
Expand Down Expand Up @@ -135,7 +140,7 @@ public function translate(
? $translation[$targetLang->getValue()]['text']
: '';
$inputWidget = new TextInputWidget([
'name' => 'translation-field-'.$tspanId,
'name' => $tspanId,
'value' => $fieldValue,
'data' => ['tspan-id' => $tspanId],
]);
Expand All @@ -161,4 +166,50 @@ public function translate(
'target_lang' => $targetLangDefault,
]);
}

/**
* @Route( "/File:{filename}", name="updownload", methods={"POST"})
*/
public function updownload(
string $filename,
Request $request,
FileCache $cache,
Uploader $uploader
): Response {
$requestParams = $request->request->all();

// Are we uploading or downloading?
$isUpload = false;
if (isset($requestParams['upload'])) {
$isUpload = true;
unset($requestParams['upload']);
}

// Target language.
$targetLang = $requestParams['target-lang'];
unset($requestParams['target-lang']);

// Add the translations to the file and save it to the filesystem.
$file = new SvgFile($cache->getPath(Title::normalize($filename)));
$file->setTranslations($targetLang, $requestParams);
$tmpFilename = $cache->fullPath($filename.uniqid().'.svg');
file_put_contents($tmpFilename, $file->saveToString());

// Download or upload.
if (!$isUpload) {
// Prompt for download.
return new BinaryFileResponse(
$tmpFilename,
200,
[],
false,
ResponseHeaderBag::DISPOSITION_ATTACHMENT
);
} else {
// Send to Commons.
$url = $uploader->upload($tmpFilename, $filename);
$this->addFlash('upload-complete', $url);
return $this->redirectToRoute('translate', ['filename' => $filename]);
}
}
}
13 changes: 13 additions & 0 deletions src/Service/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ protected function statFile(string $fileName): bool
return true;
}

/**
* Delete a cached file, if it exists.
*
* @param string $fileName The name file to delete, with no path component.
*/
public function delete(string $fileName): void
{
$path = $this->fullPath($fileName);
if (is_file($path)) {
unlink($path);
}
}

/**
* Returns a full path for the given file
*
Expand Down
69 changes: 68 additions & 1 deletion src/Service/MediaWikiApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
namespace App\Service;

use App\Exception\ImageNotFoundException;
use CURLFile;
use Exception;
use GuzzleHttp\Client;
use MediaWiki\OAuthClient\Client as OauthClient;
use MediaWiki\OAuthClient\Token;
use stdClass;
use Symfony\Component\HttpFoundation\Session\Session;

/**
* A service for interacting with MediaWiki API
Expand All @@ -16,12 +22,20 @@ class MediaWikiApi
*/
private $entryPoint;

/** @var OauthClient */
protected $oauthClient;

/** @var Token */
protected $oauthAccessToken;

/**
* @param string $entryPoint Fully-qualified URL of the wiki's api.php
*/
public function __construct(string $entryPoint)
public function __construct(string $entryPoint, OauthClient $client, Session $session)
{
$this->entryPoint = $entryPoint;
$this->oauthClient = $client;
$this->oauthAccessToken = $session->get('oauth.access_token');
}

/**
Expand Down Expand Up @@ -72,4 +86,57 @@ protected function request(array $params, string $method = 'GET'): array
$response = $client->request($method, '', $requestOptions);
return \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
}

/**
* Upload a file to the OAuth wiki.
* @param string $file The filesystem path to the file to upload.
* @param string $destinationFilename The filename to give the file on the wiki.
* @return stdClass Information about the upload.
* @throws Exception If the CSRF token can not be retrieved or the upload was not successful.
*/
public function upload(string $file, string $destinationFilename): stdClass
{
// 1. Get the CSRF token.
$csrfTokenParams = [
'format' => 'json',
'action' => 'query',
'meta' => 'tokens',
'type' => 'csrf',
];
$csrfTokenResponse = $this->oauthClient->makeOAuthCall(
$this->oauthAccessToken,
$this->entryPoint,
true,
$csrfTokenParams
);
$csrfTokenData = \GuzzleHttp\json_decode($csrfTokenResponse);
if (!isset($csrfTokenData->query->tokens->csrftoken)) {
throw new Exception("Unable to get CSRF token from: $csrfTokenResponse");
}

// 2. Upload the file.
$uploadParams = [
'format' => 'json',
'action' => 'upload',
'filename' => $destinationFilename,
'token' => $csrfTokenData->query->tokens->csrftoken,
'comment' => 'Uploaded from SVG Translate.',
'filesize' => filesize($file),
'file' => new CURLFile($file),
// We have to ignore warnings so that we can overwrite the existing image.
'ignorewarnings' => true,
];
$uploadResponse = $this->oauthClient->makeOAuthCall(
$this->oauthAccessToken,
$this->entryPoint,
true,
$uploadParams
);
$uploadResponseData = \GuzzleHttp\json_decode($uploadResponse);
if (!isset($uploadResponseData->upload->result) || 'Success' !== $uploadResponseData->upload->result) {
throw new Exception('Upload failed. Response was: '.$uploadResponse);
}

return $uploadResponseData->upload;
}
}

0 comments on commit 348c087

Please sign in to comment.