summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/controllers/admin_languages.php109
-rw-r--r--core/controllers/l10n_client.php2
-rw-r--r--core/helpers/MY_remote.php163
-rw-r--r--core/helpers/l10n_client.php209
-rw-r--r--core/views/admin_languages.html.php9
5 files changed, 485 insertions, 7 deletions
diff --git a/core/controllers/admin_languages.php b/core/controllers/admin_languages.php
index 4e9e45cd..0953d285 100644
--- a/core/controllers/admin_languages.php
+++ b/core/controllers/admin_languages.php
@@ -18,10 +18,16 @@
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
class Admin_Languages_Controller extends Admin_Controller {
- public function index() {
+ public function index($share_translations_form=null) {
$v = new Admin_View("admin.html");
$v->content = new View("admin_languages.html");
- $v->content->form = $this->_languages_form();
+ $v->content->settings_form = $this->_languages_form();
+ $v->content->update_translations_form = $this->_translation_updates_form();
+ if (empty($share_translations_form)) {
+ $share_translations_form = $this->_share_translations_form();
+ }
+ $v->content->share_translations_form = $share_translations_form;
+ $this->_outgoing_translations_count();
print $v;
}
@@ -35,16 +41,73 @@ class Admin_Languages_Controller extends Admin_Controller {
url::redirect("admin/languages");
}
+ public function fetch_updates() {
+ // TODO: Convert this to AJAX / progress bar.
+ $form = $this->_translation_updates_form();
+ if ($form->validate()) {
+ L10n_Scanner::instance()->update_index();
+ l10n_client::fetch_updates();
+ message::success(t("Translations installed/updated"));
+ }
+ url::redirect("admin/languages");
+ }
+
+ public function share() {
+ $form = $this->_share_translations_form();
+ if (!$form->validate()) {
+ // Show the page with form errors
+ return $this->index($form);
+ }
+
+ if ($form->sharing->share) {
+ l10n_client::submit_translations();
+ message::success(t("Translations submitted"));
+ } else {
+ return $this->_save_api_key($form);
+ }
+ url::redirect("admin/languages");
+ }
+
+ private function _save_api_key($form) {
+ $new_key = $form->sharing->api_key->value;
+ if ($new_key && !l10n_client::validate_api_key($new_key)) {
+ $form->sharing->api_key->add_error("invalid", 1);
+ $valid = false;
+ } else {
+ $valid = true;
+ }
+
+ if ($valid) {
+ $old_key = l10n_client::api_key();
+ l10n_client::api_key($new_key);
+ if ($old_key && !$new_key) {
+ message::success(t("Your API key has been cleared."));
+ } else if ($old_key && $new_key && $old_key != $new_key) {
+ message::success(t("Your API key has been changed."));
+ } else if (!$old_key && $new_key) {
+ message::success(t("Your API key has been saved."));
+ }
+
+ log::success(t("core"), t("l10n_client API key changed."));
+ url::redirect("admin/languages");
+ } else {
+ // Show the page with form errors
+ $this->index($form);
+ }
+ }
+
private function _languages_form() {
$all_locales = locale::available();
$installed_locales = locale::installed();
$form = new Forge("admin/languages/save", "", "post", array("id" => "gLanguageSettingsForm"));
$group = $form->group("choose_language")
- ->label(t("Please select a language"));
+ ->label(t("Language settings"));
$group->dropdown("locale")
->options($installed_locales)
->selected(module::get_var("core", "default_locale"))
- ->rules("required");
+ ->label(t("Default language"))
+ ->rules('required');
+
$installation_options = array();
foreach ($all_locales as $code => $display_name) {
$installation_options[$code] = array($display_name, isset($installed_locales->$code));
@@ -56,5 +119,43 @@ class Admin_Languages_Controller extends Admin_Controller {
$group->submit("save")->value(t("Save settings"));
return $form;
}
+
+ private function _translation_updates_form() {
+ // TODO: Show a timestamp of the last update.
+ // TODO: Show a note if you've changed the language settings but not fetched translations for
+ // the selected languages yet.
+ $form = new Forge("admin/languages/fetch_updates", "", "post", array("id" => "gLanguageUpdatesForm"));
+ $group = $form->group("updates")
+ ->label(t("Download translations for all selected languages from the Gallery Translation Server:"));
+ $group->submit("update")->value(t("Get updates"));
+ return $form;
+ }
+
+ private function _outgoing_translations_count() {
+ return Database::instance()
+ ->query("SELECT COUNT(*) AS `C` FROM outgoing_translations")
+ ->current()->C;
+ }
+
+ private function _share_translations_form() {
+ $form = new Forge("admin/languages/share", "", "post", array("id" => "gShareTranslationsForm"));
+ $group = $form->group("sharing")
+ ->label(t("Sharing you own translations with the Gallery community is easy. Please do!"));
+ $api_key = l10n_client::api_key();
+ $server_link = l10n_client::server_api_key_url();
+ $group->input("api_key")
+ ->label(empty($api_key)
+ ? t("This is a unique key that will allow you to send translations to the remote server. To get your API key go to %server-link.",
+ array("server-link" => html::anchor($server_link)))
+ : t("API Key"))
+ ->value($api_key)
+ ->error_messages("invalid", t("The API key you provided is invalid."));
+ $group->submit("save")->value(t("Save settings"));
+ if ($api_key && $this->_outgoing_translations_count()) {
+ // TODO: UI improvement: hide API key / save button when API key is set.
+ $group->submit("share")->value(t("Submit translations"));
+ }
+ return $form;
+ }
}
diff --git a/core/controllers/l10n_client.php b/core/controllers/l10n_client.php
index 3e95248a..68340ed2 100644
--- a/core/controllers/l10n_client.php
+++ b/core/controllers/l10n_client.php
@@ -18,7 +18,7 @@
* Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
*/
class L10n_Client_Controller extends Controller {
- public function save($string) {
+ public function save() {
access::verify_csrf();
user::active()->admin or access::forbidden();
diff --git a/core/helpers/MY_remote.php b/core/helpers/MY_remote.php
new file mode 100644
index 00000000..52e10aac
--- /dev/null
+++ b/core/helpers/MY_remote.php
@@ -0,0 +1,163 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2008 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class remote extends remote_Core {
+
+ static function post($url, $post_data_array, $extra_headers=array()) {
+ $post_data_raw = self::_encode_post_data($post_data_array, $extra_headers);
+
+ /* Read the web page into a buffer */
+ list ($response_status, $response_headers, $response_body) =
+ self::do_request($url, 'POST', $extra_headers, $post_data_raw);
+
+ return array($response_body, $response_status, $response_headers);
+ }
+
+ static function success($response_status) {
+ return preg_match("/^HTTP\/\d+\.\d+\s2\d{2}(\s|$)/", trim($response_status));
+ }
+
+ /**
+ * Encode the post data. For each key/value pair, urlencode both the key and the value and then
+ * concatenate together. As per the specification, each key/value pair is separated with an
+ * ampersand (&)
+ * @param array $post_data_array the key/value post data
+ * @param array $extra_headers extra headers to pass to the server
+ * @return string the encoded post data
+ */
+ private static function _encode_post_data($post_data_array, &$extra_headers) {
+ $post_data_raw = '';
+ foreach ($post_data_array as $key => $value) {
+ if (!empty($post_data_raw)) {
+ $post_data_raw .= '&';
+ }
+ $post_data_raw .= urlencode($key) . '=' . urlencode($value);
+ }
+
+ $extra_headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ $extra_headers['Content-Length'] = strlen($post_data_raw);
+
+ return $post_data_raw;
+ }
+
+ /**
+ * A single request, without following redirects
+ *
+ * @todo: Handle redirects? If so, only for GET (i.e. not for POST), and use G2's WebHelper_simple::_parseLocation logic.
+ */
+ static function do_request($url, $method='GET', $headers=array(), $body='') {
+ /* Convert illegal characters */
+ $url = str_replace(' ', '%20', $url);
+
+ $url_components = self::_parse_url_for_fsockopen($url);
+ $handle = fsockopen(
+ $url_components['fsockhost'], $url_components['port'], $errno, $errstr, 5);
+ if (empty($handle)) {
+ // log "Error $errno: '$errstr' requesting $url";
+ return array(null, null, null);
+ }
+
+ $header_lines = array('Host: ' . $url_components['host']);
+ foreach ($headers as $key => $value) {
+ $header_lines[] = $key . ': ' . $value;
+ }
+
+ $success = fwrite($handle, sprintf("%s %s HTTP/1.0\r\n%s\r\n\r\n%s",
+ $method,
+ $url_components['uri'],
+ implode("\r\n", $header_lines),
+ $body));
+ if (!$success) {
+ // Zero bytes written or false was returned
+ // log "fwrite failed in requestWebPage($url)" . ($success === false ? ' - false' : ''
+ return array(null, null, null);
+ }
+ fflush($handle);
+
+ /*
+ * Read the status line. fgets stops after newlines. The first line is the protocol
+ * version followed by a numeric status code and its associated textual phrase.
+ */
+ $response_status = trim(fgets($handle, 4096));
+ if (empty($response_status)) {
+ // 'Empty http response code, maybe timeout'
+ return array(null, null, null);
+ }
+
+ /* Read the headers */
+ $response_headers = array();
+ while (!feof($handle)) {
+ $line = trim(fgets($handle, 4096));
+ if (empty($line)) {
+ break;
+ }
+
+ /* Normalize the line endings */
+ $line = str_replace("\r", '', $line);
+
+ list ($key, $value) = explode(':', $line, 2);
+ if (isset($response_headers[$key])) {
+ if (!is_array($response_headers[$key])) {
+ $response_headers[$key] = array($response_headers[$key]);
+ }
+ $response_headers[$key][] = trim($value);
+ } else {
+ $response_headers[$key] = trim($value);
+ }
+ }
+
+ /* Read the body */
+ $response_body = '';
+ while (!feof($handle)) {
+ $response_body .= fread($handle, 4096);
+ }
+ fclose($handle);
+
+ return array($response_status, $response_headers, $response_body);
+ }
+
+ /**
+ * Prepare for fsockopen call.
+ * @param string $url
+ * @return array url components
+ * @access private
+ */
+ private static function _parse_url_for_fsockopen($url) {
+ $url_components = parse_url($url);
+ if (strtolower($url_components['scheme']) == 'https') {
+ $url_components['fsockhost'] = 'ssl://' . $url_components['host'];
+ $default_port = 443;
+ } else {
+ $url_components['fsockhost'] = $url_components['host'];
+ $default_port = 80;
+ }
+ if (empty($url_components['port'])) {
+ $url_components['port'] = $default_port;
+ }
+ if (empty($url_components['path'])) {
+ $url_components['path'] = '/';
+ }
+ $uri = $url_components['path']
+ . (empty($url_components['query']) ? '' : '?' . $url_components['query']);
+ /* Unescape ampersands, since if the url comes from form input it will be escaped */
+ $url_components['uri'] = str_replace('&amp;', '&', $uri);
+ return $url_components;
+ }
+}
+?>
diff --git a/core/helpers/l10n_client.php b/core/helpers/l10n_client.php
new file mode 100644
index 00000000..da841116
--- /dev/null
+++ b/core/helpers/l10n_client.php
@@ -0,0 +1,209 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2008 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class l10n_client_Core {
+
+ private static function server_url() {
+ return "http://gallery.menalto.com/index.php";
+ }
+
+ static function server_api_key_url() {
+ return self::server_url() . "?q=translations/userkey/"
+ . self::client_token();
+ }
+
+ static function client_token() {
+ return md5('l10n_client_client_token' . access::private_key());
+ }
+
+ static function api_key($api_key=null) {
+ if ($api_key !== null) {
+ module::set_var("core", "l10n_client_key", $api_key);
+ }
+ return module::get_var("core", "l10n_client_key", "");
+ }
+
+ static function server_uid($api_key=null) {
+ $api_key = $api_key == null ? self::api_key() : $api_key;
+ $parts = explode(":", $api_key);
+ return empty($parts) ? 0 : $parts[0];
+ }
+
+ private static function _sign($payload, $api_key=null) {
+ $api_key = $api_key == null ? self::api_key() : $api_key;
+ return md5($api_key . $payload . self::client_token());
+ }
+
+ static function validate_api_key($api_key) {
+ $version = "1.0";
+ $url = self::server_url() . "?q=translations/status";
+ $signature = self::_sign($version, $api_key);
+
+ list ($response_data, $response_status) = remote::post($url, array("version" => $version,
+ "client_token" => self::client_token(),
+ "signature" => $signature,
+ "uid" => self::server_uid($api_key)));
+ if (!remote::success($response_status)) {
+ return false;
+ }
+ return true;
+ }
+
+ static function fetch_updates() {
+ $request->locales = array();
+ $request->messages = new stdClass();
+
+ $locales = locale::installed();
+ foreach ($locales as $locale => $locale_data) {
+ $request->locales[] = $locale;
+ }
+
+ // TODO: Batch requests (max request size)
+
+ foreach (Database::instance()
+ ->select("key", "locale", "revision", "translation")
+ ->from("incoming_translations")
+ ->get()
+ ->as_array() as $row) {
+ if (!isset($request->messages->{$row->key})) {
+ $request->messages->{$row->key} = 1;
+ }
+ if (!empty($row->revision) && !empty($row->translation)) {
+ if (!is_object($request->messages->{$row->key})) {
+ $request->messages->{$row->key} = new stdClass();
+ }
+ $request->messages->{$row->key}->{$row->locale} = $row->revision;
+ }
+ }
+ // TODO: Include messages from outgoing_translations?
+
+ $request_data = json_encode($request);
+ $url = self::server_url() . "?q=translations/fetch";
+ list ($response_data, $response_status) = remote::post($url, array("data" => $request_data));
+ if (!remote::success($response_status)) {
+ throw new Exception("Translations fetch request failed with: " . $response_status);
+ }
+ if (empty($response_data)) {
+ log::info(t("translations"), t("Translations fetch request resulted in an emptyu response."));
+ return;
+ }
+
+ $response = json_decode($response_data);
+
+ /*
+ * Response format (JSON payload):
+ * [{key:<key_1>, translation: <JSON encoded translation>, rev:<rev>, locale:<locale>},
+ * {key:<key_2>, ...}
+ * ]
+ */
+ log::info(t("translations"), t2("Installed 1 new / updated translation message.",
+ "Installed %count new / updated translation messages.",
+ count($response)));
+
+ foreach ($response as $message_data) {
+ // TODO: Better input validation
+ if (empty($message_data->key) || empty($message_data->translation) ||
+ empty($message_data->locale) || empty($message_data->rev)) {
+ throw new Exception("Translations fetch request resulted in Invalid response data");
+ }
+ $key = $message_data->key;
+ $locale = $message_data->locale;
+ $revision = $message_data->rev;
+ $translation = serialize(json_decode($message_data->translation));
+ // TODO: Should we normalize the incoming_translations table into messages(id, key, message)
+ // and incoming_translations(id, translation, locale, revision)?
+ // Or just allow incoming_translations.message to be NULL?
+ $locale = $message_data->locale;
+ $entry = ORM::factory("incoming_translation")
+ ->where(array("key" => $key, "locale" => $locale))
+ ->find();
+ if (!$entry->loaded) {
+ // TODO: Load a message key -> message (text) dict into memory outside of this loop
+ $root_entry = ORM::factory("incoming_translation")
+ ->where(array("key" => $key, "locale" => "root"))
+ ->find();
+ $entry->key = $key;
+ $entry->message = $root_entry->message;
+ $entry->locale = $locale;
+ }
+ $entry->revision = $revision;
+ $entry->translation = $translation;
+ $entry->save();
+ }
+ }
+
+ static function submit_translations() {
+ /*
+ * Request format (HTTP POST):
+ * client_token = <client_token>
+ * uid = <l10n server user id>
+ * signature = md5(user_api_key($uid, $client_token) . $data . $client_token))
+ * data = // JSON payload
+ *
+ * {<key_1>: {message: <JSON encoded message>
+ * translations: {<locale_1>: <JSON encoded translation>,
+ * <locale_2>: ...}},
+ * <key_2>: {...}
+ * }
+ */
+
+ // TODO: Batch requests (max request size)
+ // TODO: include base_revision in submission / how to handle resubmissions / edit fights?
+ foreach (Database::instance()
+ ->select("key", "message", "locale", "base_revision", "translation")
+ ->from("outgoing_translations")
+ ->get()
+ ->as_array() as $row) {
+ $key = $row->key;
+ if (!isset($request->{$key})) {
+ $request->{$key}->message = json_encode(unserialize($row->message));
+ }
+ $request->{$key}->translations->{$row->locale} = json_encode(unserialize($row->translation));
+ }
+
+ // TODO: reduce memory consumpotion, e.g. free $request
+ $request_data = json_encode($request);
+ $url = self::server_url() . "?q=translations/submit";
+ $signature = self::_sign($request_data);
+
+ list ($response_data, $response_status) = remote::post($url, array("data" => $request_data,
+ "client_token" => self::client_token(),
+ "signature" => $signature,
+ "uid" => self::server_uid()));
+
+ if (!remote::success($response_status)) {
+ throw new Exception("Translations submission failed with: " . $response_status);
+ }
+ if (empty($response_data)) {
+ return;
+ }
+
+ $response = json_decode($response_data);
+ /*
+ * Response format (JSON payload):
+ * [{key:<key_1>, locale:<locale_1>, rev:<rev_1>, status:<rejected|accepted|pending>},
+ * {key:<key_2>, ...}
+ * ]
+ *
+ */
+ // TODO: Move messages out of outgoing into incoming, using new rev?
+ // TODO: show which messages have been rejected / are pending?
+ }
+} \ No newline at end of file
diff --git a/core/views/admin_languages.html.php b/core/views/admin_languages.html.php
index b0f6fa26..abeaf362 100644
--- a/core/views/admin_languages.html.php
+++ b/core/views/admin_languages.html.php
@@ -1,6 +1,11 @@
<?php defined("SYSPATH") or die("No direct script access.") ?>
<div id="gLanguages">
- <h1> <?= t("Language Settings") ?> </h1>
+ <h2> <?= t("Languages") ?> </h2>
- <?= $form ?>
+ <?= $settings_form ?>
+
+ <?= $update_translations_form ?>
+
+ <h2> <?= t("Your Own Translations") ?> </h2>
+ <?= $share_translations_form ?>
</div>