diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/controllers/admin_languages.php | 109 | ||||
-rw-r--r-- | core/controllers/l10n_client.php | 2 | ||||
-rw-r--r-- | core/helpers/MY_remote.php | 163 | ||||
-rw-r--r-- | core/helpers/l10n_client.php | 209 | ||||
-rw-r--r-- | core/views/admin_languages.html.php | 9 |
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('&', '&', $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> |