From: ArthurHoaro Date: Tue, 25 Apr 2017 17:09:13 +0000 (+0200) Subject: Merge pull request #830 from ArthurHoaro/theme/timezone X-Git-Tag: v0.9.0~13 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=4c7045229c94973c1cb83193e69463f426ddc35b;hp=49bc541d7973bd86776441eb072e66d01b368e68;p=github%2Fshaarli%2FShaarli.git Merge pull request #830 from ArthurHoaro/theme/timezone Change timezone data structure send to the templates --- diff --git a/application/LinkDB.php b/application/LinkDB.php index 4cee2af9..1e4d7ce8 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -144,10 +144,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess if (!isset($value['id']) || empty($value['url'])) { die('Internal Error: A link should always have an id and URL.'); } - if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { + if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { die('You must specify an integer as a key.'); } - if (! empty($offset) && $offset !== $value['id']) { + if ($offset !== null && $offset !== $value['id']) { die('Array offset and link ID must be equal.'); } diff --git a/application/Utils.php b/application/Utils.php index 5c077450..ab463af9 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -321,25 +321,117 @@ function normalize_spaces($string) * otherwise default format '%c' will be returned. * * @param DateTime $date to format. + * @param bool $time Displays time if true. * @param bool $intl Use international format if true. * * @return bool|string Formatted date, or false if the input is invalid. */ -function format_date($date, $intl = true) +function format_date($date, $time = true, $intl = true) { if (! $date instanceof DateTime) { return false; } if (! $intl || ! class_exists('IntlDateFormatter')) { - return strftime('%c', $date->getTimestamp()); + $format = $time ? '%c' : '%x'; + return strftime($format, $date->getTimestamp()); } $formatter = new IntlDateFormatter( setlocale(LC_TIME, 0), IntlDateFormatter::LONG, - IntlDateFormatter::LONG + $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE ); return $formatter->format($date); } + +/** + * Check if the input is an integer, no matter its real type. + * + * PHP is a bit messy regarding this: + * - is_int returns false if the input is a string + * - ctype_digit returns false if the input is an integer or negative + * + * @param mixed $input value + * + * @return bool true if the input is an integer, false otherwise + */ +function is_integer_mixed($input) +{ + if (is_array($input) || is_bool($input) || is_object($input)) { + return false; + } + $input = strval($input); + return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1))); +} + +/** + * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. + * + * @param string $val Size expressed in string. + * + * @return int Size expressed in bytes. + */ +function return_bytes($val) +{ + if (is_integer_mixed($val) || $val === '0' || empty($val)) { + return $val; + } + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = intval(substr($val, 0, -1)); + switch($last) { + case 'g': $val *= 1024; + case 'm': $val *= 1024; + case 'k': $val *= 1024; + } + return $val; +} + +/** + * Return a human readable size from bytes. + * + * @param int $bytes value + * + * @return string Human readable size + */ +function human_bytes($bytes) +{ + if ($bytes === '') { + return t('Setting not set'); + } + if (! is_integer_mixed($bytes)) { + return $bytes; + } + $bytes = intval($bytes); + if ($bytes === 0) { + return t('Unlimited'); + } + + $units = [t('B'), t('kiB'), t('MiB'), t('GiB')]; + for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) { + $bytes /= 1024; + } + + return round($bytes) . $units[$i]; +} + +/** + * Try to determine max file size for uploads (POST). + * Returns an integer (in bytes) or formatted depending on $format. + * + * @param mixed $limitPost post_max_size PHP setting + * @param mixed $limitUpload upload_max_filesize PHP setting + * @param bool $format Format max upload size to human readable size + * + * @return int|string max upload file size + */ +function get_max_upload_size($limitPost, $limitUpload, $format = true) +{ + $size1 = return_bytes($limitPost); + $size2 = return_bytes($limitUpload); + // Return the smaller of two: + $maxsize = min($size1, $size2); + return $format ? human_bytes($maxsize) : $maxsize; +} diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index d4015865..b8155a34 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -12,7 +12,7 @@ class ApiUtils /** * Validates a JWT token authenticity. * - * @param string $token JWT token extracted from the headers. + * @param string $token JWT token extracted from the headers. * @param string $secret API secret set in the settings. * * @throws ApiAuthorizationException the token is not valid. @@ -50,7 +50,7 @@ class ApiUtils /** * Format a Link for the REST API. * - * @param array $link Link data read from the datastore. + * @param array $link Link data read from the datastore. * @param string $indexUrl Shaarli's index URL (used for relative URL). * * @return array Link data formatted for the REST API. @@ -77,4 +77,35 @@ class ApiUtils } return $out; } + + /** + * Convert a link given through a request, to a valid link for LinkDB. + * + * If no URL is provided, it will generate a local note URL. + * If no title is provided, it will use the URL as title. + * + * @param array $input Request Link. + * @param bool $defaultPrivate Request Link. + * + * @return array Formatted link. + */ + public static function buildLinkFromRequest($input, $defaultPrivate) + { + $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : ''; + if (isset($input['private'])) { + $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN); + } else { + $private = $defaultPrivate; + } + + $link = [ + 'title' => ! empty($input['title']) ? $input['title'] : $input['url'], + 'url' => $input['url'], + 'description' => ! empty($input['description']) ? $input['description'] : '', + 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '', + 'private' => $private, + 'created' => new \DateTime(), + ]; + return $link; + } } diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index 1dd47f17..f35b923a 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php @@ -51,4 +51,14 @@ abstract class ApiController $this->jsonStyle = null; } } + + /** + * Get the container. + * + * @return Container + */ + public function getCi() + { + return $this->ci; + } } diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index d4f1a09c..0db10fd0 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -97,11 +97,53 @@ class Links extends ApiController */ public function getLink($request, $response, $args) { - if (! isset($this->linkDb[$args['id']])) { + if (!isset($this->linkDb[$args['id']])) { throw new ApiLinkNotFoundException(); } $index = index_url($this->ci['environment']); $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); + return $response->withJson($out, 200, $this->jsonStyle); } + + /** + * Creates a new link from posted request body. + * + * @param Request $request Slim request. + * @param Response $response Slim response. + * + * @return Response response. + */ + public function postLink($request, $response) + { + $data = $request->getParsedBody(); + $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); + // duplicate by URL, return 409 Conflict + if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) { + return $response->withJson( + ApiUtils::formatLink($dup, index_url($this->ci['environment'])), + 409, + $this->jsonStyle + ); + } + + $link['id'] = $this->linkDb->getNextId(); + $link['shorturl'] = link_small_hash($link['created'], $link['id']); + + // note: general relative URL + if (empty($link['url'])) { + $link['url'] = '?' . $link['shorturl']; + } + + if (empty($link['title'])) { + $link['title'] = $link['url']; + } + + $this->linkDb[$link['id']] = $link; + $this->linkDb->save($this->conf->get('resource.page_cache')); + $out = ApiUtils::formatLink($link, index_url($this->ci['environment'])); + $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]); + return $response->withAddedHeader('Location', $redirect) + ->withJson($out, 201, $this->jsonStyle); + } } diff --git a/index.php b/index.php index 5497a23e..e392e501 100644 --- a/index.php +++ b/index.php @@ -432,7 +432,7 @@ if (isset($_POST['login'])) // Optional redirect after login: if (isset($_GET['post'])) { $uri = '?post='. urlencode($_GET['post']); - foreach (array('description', 'source', 'title') as $param) { + foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $uri .= '&'.$param.'='.urlencode($_GET[$param]); } @@ -461,7 +461,7 @@ if (isset($_POST['login'])) $redir = '&username='. $_POST['login']; if (isset($_GET['post'])) { $redir .= '&post=' . urlencode($_GET['post']); - foreach (array('description', 'source', 'title') as $param) { + foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $redir .= '&' . $param . '=' . urlencode($_GET[$param]); } @@ -472,34 +472,6 @@ if (isset($_POST['login'])) } } -// ------------------------------------------------------------------------------------------ -// Misc utility functions: - -// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. -function return_bytes($val) -{ - $val = trim($val); $last=strtolower($val[strlen($val)-1]); - switch($last) - { - case 'g': $val *= 1024; - case 'm': $val *= 1024; - case 'k': $val *= 1024; - } - return $val; -} - -// Try to determine max file size for uploads (POST). -// Returns an integer (in bytes) -function getMaxFileSize() -{ - $size1 = return_bytes(ini_get('post_max_size')); - $size2 = return_bytes(ini_get('upload_max_filesize')); - // Return the smaller of two: - $maxsize = min($size1,$size2); - // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000) - return $maxsize; -} - // ------------------------------------------------------------------------------------------ // Token management for XSRF protection // Token should be used in any form which acts on data (create,update,delete,import...). @@ -695,9 +667,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); $data = array( + 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false), 'linksToDisplay' => $linksToDisplay, 'cols' => $columns, 'day' => $dayDate->getTimestamp(), + 'dayDate' => $dayDate, 'previousday' => $previousday, 'nextday' => $nextday, ); @@ -1044,7 +1018,13 @@ function renderPage($conf, $pluginManager, $LINKSDB) // Show login screen, then redirect to ?post=... if (isset($_GET['post'])) { - header('Location: ?do=login&post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link. + header( // Redirect to login page, then back to post link. + 'Location: ?do=login&post='.urlencode($_GET['post']). + (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):''). + (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):''). + (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):''). + (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'') + ); exit; } @@ -1141,7 +1121,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks'])); $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); - $conf->set('api.enabled', !empty($_POST['apiEnabled'])); + $conf->set('api.enabled', !empty($_POST['enableApi'])); $conf->set('api.secret', escape($_POST['apiSecret'])); try { $conf->write(isLoggedIn()); @@ -1248,7 +1228,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) } // lf_id should only be present if the link exists. - $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); + $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); // Linkdate is kept here to: // - use the same permalink for notes as they're displayed when creating them // - let users hack creation date of their posts @@ -1321,9 +1301,13 @@ function renderPage($conf, $pluginManager, $LINKSDB) // -------- User clicked the "Cancel" button when editing a link. if (isset($_POST['cancel_edit'])) { + $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false; + if (! isset($LINKSDB[$id])) { + header('Location: ?'); + } // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo ''; exit; } - $link = $LINKSDB[(int) escape($_POST['lf_id'])]; + $link = $LINKSDB[$id]; $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); // Scroll to the link which has been edited. $returnurl .= '#'. $link['shorturl']; @@ -1508,7 +1492,22 @@ function renderPage($conf, $pluginManager, $LINKSDB) if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { // Show import dialog - $PAGE->assign('maxfilesize', getMaxFileSize()); + $PAGE->assign( + 'maxfilesize', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + false + ) + ); + $PAGE->assign( + 'maxfilesizeHuman', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + true + ) + ); $PAGE->renderPage('import'); exit; } @@ -1518,7 +1517,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) // The file is too big or some form field may be missing. echo ''; exit; @@ -2227,9 +2226,10 @@ $app = new \Slim\App($container); // REST API routes $app->group('/api/v1', function() { - $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo'); - $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks'); - $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink'); + $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo'); + $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks'); + $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink'); + $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); })->add('\Shaarli\Api\ApiMiddleware'); $response = $app->run(true); diff --git a/plugins/readityourself/book-open.png b/plugins/readityourself/book-open.png deleted file mode 100644 index 36513d7b..00000000 Binary files a/plugins/readityourself/book-open.png and /dev/null differ diff --git a/plugins/readityourself/readityourself.html b/plugins/readityourself/readityourself.html deleted file mode 100644 index 5e200715..00000000 --- a/plugins/readityourself/readityourself.html +++ /dev/null @@ -1 +0,0 @@ -readityourself diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta deleted file mode 100644 index bd611dd0..00000000 --- a/plugins/readityourself/readityourself.meta +++ /dev/null @@ -1,2 +0,0 @@ -description="For each link, add a ReadItYourself icon to save the shaared URL." -parameters=READITYOUSELF_URL; \ No newline at end of file diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php deleted file mode 100644 index 961c5bda..00000000 --- a/plugins/readityourself/readityourself.php +++ /dev/null @@ -1,51 +0,0 @@ -get('plugins.READITYOUSELF_URL'); - if (empty($riyUrl)) { - $error = 'Readityourself plugin error: '. - 'Please define the "READITYOUSELF_URL" setting in the plugin administration page.'; - return array($error); - } -} - -/** - * Add readityourself icon to link_plugin when rendering linklist. - * - * @param mixed $data Linklist data. - * @param ConfigManager $conf Configuration Manager instance. - * - * @return mixed - linklist data with readityourself plugin. - */ -function hook_readityourself_render_linklist($data, $conf) -{ - $riyUrl = $conf->get('plugins.READITYOUSELF_URL'); - if (empty($riyUrl)) { - return $data; - } - - $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html'); - - foreach ($data['links'] as &$value) { - $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH); - $value['link_plugin'][] = $readityourself; - } - - return $data; -} diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index e70cc1ae..d6a0aad5 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -4,6 +4,7 @@ */ require_once 'application/Utils.php'; +require_once 'application/Languages.php'; require_once 'tests/utils/ReferenceSessionIdHashes.php'; // Initialize reference data before PHPUnit starts a session @@ -326,4 +327,94 @@ class UtilsTest extends PHPUnit_Framework_TestCase $this->assertFalse(format_date([])); $this->assertFalse(format_date(null)); } + + /** + * Test is_integer_mixed with valid values + */ + public function testIsIntegerMixedValid() + { + $this->assertTrue(is_integer_mixed(12)); + $this->assertTrue(is_integer_mixed('12')); + $this->assertTrue(is_integer_mixed(-12)); + $this->assertTrue(is_integer_mixed('-12')); + $this->assertTrue(is_integer_mixed(0)); + $this->assertTrue(is_integer_mixed('0')); + $this->assertTrue(is_integer_mixed(0x0a)); + } + + /** + * Test is_integer_mixed with invalid values + */ + public function testIsIntegerMixedInvalid() + { + $this->assertFalse(is_integer_mixed(true)); + $this->assertFalse(is_integer_mixed(false)); + $this->assertFalse(is_integer_mixed([])); + $this->assertFalse(is_integer_mixed(['test'])); + $this->assertFalse(is_integer_mixed([12])); + $this->assertFalse(is_integer_mixed(new DateTime())); + $this->assertFalse(is_integer_mixed('0x0a')); + $this->assertFalse(is_integer_mixed('12k')); + $this->assertFalse(is_integer_mixed('k12')); + $this->assertFalse(is_integer_mixed('')); + } + + /** + * Test return_bytes + */ + public function testReturnBytes() + { + $this->assertEquals(2 * 1024, return_bytes('2k')); + $this->assertEquals(2 * 1024, return_bytes('2K')); + $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m')); + $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M')); + $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g')); + $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G')); + $this->assertEquals(374, return_bytes('374')); + $this->assertEquals(374, return_bytes(374)); + $this->assertEquals(0, return_bytes('0')); + $this->assertEquals(0, return_bytes(0)); + $this->assertEquals(-1, return_bytes('-1')); + $this->assertEquals(-1, return_bytes(-1)); + $this->assertEquals('', return_bytes('')); + } + + /** + * Test human_bytes + */ + public function testHumanBytes() + { + $this->assertEquals('2kiB', human_bytes(2 * 1024)); + $this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); + $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); + $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); + $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); + $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); + $this->assertEquals('374B', human_bytes(374)); + $this->assertEquals('374B', human_bytes('374')); + $this->assertEquals('232kiB', human_bytes(237481)); + $this->assertEquals('Unlimited', human_bytes('0')); + $this->assertEquals('Unlimited', human_bytes(0)); + $this->assertEquals('Setting not set', human_bytes('')); + } + + /** + * Test get_max_upload_size with formatting + */ + public function testGetMaxUploadSize() + { + $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); + $this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); + $this->assertEquals('100B', get_max_upload_size(100, 100)); + } + + /** + * Test get_max_upload_size without formatting + */ + public function testGetMaxUploadSizeRaw() + { + $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false)); + $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false)); + $this->assertEquals('100', get_max_upload_size(100, 100, false)); + } } diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/PostLinkTest.php new file mode 100644 index 00000000..3ed7bcb0 --- /dev/null +++ b/tests/api/controllers/PostLinkTest.php @@ -0,0 +1,193 @@ +conf = new ConfigManager('tests/utils/config/configJson.json.php'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); + + $this->controller = new Links($this->container); + + $mock = $this->getMock('\Slim\Router', ['relativePathFor']); + $mock->expects($this->any()) + ->method('relativePathFor') + ->willReturn('api/v1/links/1'); + + // affect @property-read... seems to work + $this->controller->getCi()->router = $mock; + + // Used by index_url(). + $this->controller->getCi()['environment'] = [ + 'SERVER_NAME' => 'domain.tld', + 'SERVER_PORT' => 80, + 'SCRIPT_NAME' => '/', + ]; + } + + /** + * After every test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + } + + /** + * Test link creation without any field: creates a blank note. + */ + public function testPostLinkMinimal() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'POST', + ]); + + $request = Request::createFromEnvironment($env); + + $response = $this->controller->postLink($request, new Response()); + $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(43, $data['id']); + $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']); + $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']); + $this->assertEquals('?' . $data['shorturl'], $data['title']); + $this->assertEquals('', $data['description']); + $this->assertEquals([], $data['tags']); + $this->assertEquals(false, $data['private']); + $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])); + $this->assertEquals('', $data['updated']); + } + + /** + * Test link creation with all available fields. + */ + public function testPostLinkFull() + { + $link = [ + 'url' => 'website.tld/test?foo=bar', + 'title' => 'new entry', + 'description' => 'shaare description', + 'tags' => ['one', 'two'], + 'private' => true, + ]; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'POST', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->postLink($request, new Response()); + + $this->assertEquals(201, $response->getStatusCode()); + $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(43, $data['id']); + $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']); + $this->assertEquals('http://' . $link['url'], $data['url']); + $this->assertEquals($link['title'], $data['title']); + $this->assertEquals($link['description'], $data['description']); + $this->assertEquals($link['tags'], $data['tags']); + $this->assertEquals(true, $data['private']); + $this->assertTrue(new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])); + $this->assertEquals('', $data['updated']); + } + + /** + * Test link creation with an existing link (duplicate URL). Should return a 409 HTTP error and the existing link. + */ + public function testPostLinkDuplicate() + { + $link = [ + 'url' => 'mediagoblin.org/', + 'title' => 'new entry', + 'description' => 'shaare description', + 'tags' => ['one', 'two'], + 'private' => true, + ]; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'POST', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->postLink($request, new Response()); + + $this->assertEquals(409, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(7, $data['id']); + $this->assertEquals('IuWvgA', $data['shorturl']); + $this->assertEquals('http://mediagoblin.org/', $data['url']); + $this->assertEquals('MediaGoblin', $data['title']); + $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']); + $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); + $this->assertEquals(false, $data['private']); + $this->assertEquals( + \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) + ); + $this->assertEquals( + \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) + ); + } +} diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php index 545fa572..6c9c9adc 100644 --- a/tests/languages/de/UtilsDeTest.php +++ b/tests/languages/de/UtilsDeTest.php @@ -11,7 +11,16 @@ class UtilsDeTest extends UtilsTest public function testDateFormat() { $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); - $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true)); + $this->assertRegExp('/1\. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true, true)); + } + + /** + * Test date_format() without time. + */ + public function testDateFormatNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/1\. Januar 2017/', format_date($date, false,true)); } /** @@ -20,7 +29,16 @@ class UtilsDeTest extends UtilsTest public function testDateFormatDefault() { $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); - $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false)); + $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, true, false)); + } + + /** + * Test date_format() using builtin PHP function strftime without time. + */ + public function testDateFormatDefaultNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170201_101112'); + $this->assertEquals('01.02.2017', format_date($date, false, false)); } /** diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php index 7c829ac7..d8680b2b 100644 --- a/tests/languages/en/UtilsEnTest.php +++ b/tests/languages/en/UtilsEnTest.php @@ -11,7 +11,16 @@ class UtilsEnTest extends UtilsTest public function testDateFormat() { $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); - $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true)); + $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true, true)); + } + + /** + * Test date_format() without time. + */ + public function testDateFormatNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/January 1, 2017/', format_date($date, false, true)); } /** @@ -20,7 +29,16 @@ class UtilsEnTest extends UtilsTest public function testDateFormatDefault() { $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); - $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false)); + $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, true, false)); + } + + /** + * Test date_format() using builtin PHP function strftime without time. + */ + public function testDateFormatDefaultNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170201_101112'); + $this->assertEquals('02/01/2017', format_date($date, false, false)); } /** diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php index 45996ee2..0d50a878 100644 --- a/tests/languages/fr/UtilsFrTest.php +++ b/tests/languages/fr/UtilsFrTest.php @@ -14,13 +14,31 @@ class UtilsFrTest extends UtilsTest $this->assertRegExp('/1 janvier 2017 (à )?10:11:12 UTC\+0?3(:00)?/', format_date($date)); } + /** + * Test date_format() without time. + */ + public function testDateFormatNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); + $this->assertRegExp('/1 janvier 2017/', format_date($date, false, true)); + } + /** * Test date_format() using builtin PHP function strftime. */ public function testDateFormatDefault() { $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); - $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false)); + $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, true, false)); + } + + /** + * Test date_format() using builtin PHP function strftime without time. + */ + public function testDateFormatDefaultNoTime() + { + $date = DateTime::createFromFormat('Ymd_His', '20170201_101112'); + $this->assertEquals('01/02/2017', format_date($date, false, false)); } /** diff --git a/tests/plugins/PluginReadityourselfTest.php b/tests/plugins/PluginReadityourselfTest.php deleted file mode 100644 index bbba9676..00000000 --- a/tests/plugins/PluginReadityourselfTest.php +++ /dev/null @@ -1,99 +0,0 @@ -set('plugins.READITYOUSELF_URL', 'value'); - $errors = readityourself_init($conf); - $this->assertEmpty($errors); - } - - /** - * Test Readityourself init with errors. - */ - public function testReadityourselfInitError() - { - $conf = new ConfigManager(''); - $errors = readityourself_init($conf); - $this->assertNotEmpty($errors); - } - - /** - * Test render_linklist hook. - */ - public function testReadityourselfLinklist() - { - $conf = new ConfigManager(''); - $conf->set('plugins.READITYOUSELF_URL', 'value'); - $str = 'http://randomstr.com/test'; - $data = array( - 'title' => $str, - 'links' => array( - array( - 'url' => $str, - ) - ) - ); - - $data = hook_readityourself_render_linklist($data, $conf); - $link = $data['links'][0]; - // data shouldn't be altered - $this->assertEquals($str, $data['title']); - $this->assertEquals($str, $link['url']); - - // plugin data - $this->assertEquals(1, count($link['link_plugin'])); - $this->assertNotFalse(strpos($link['link_plugin'][0], $str)); - } - - /** - * Test without config: nothing should happened. - */ - public function testReadityourselfLinklistWithoutConfig() - { - $conf = new ConfigManager(''); - $conf->set('plugins.READITYOUSELF_URL', null); - $str = 'http://randomstr.com/test'; - $data = array( - 'title' => $str, - 'links' => array( - array( - 'url' => $str, - ) - ) - ); - - $data = hook_readityourself_render_linklist($data, $conf); - $link = $data['links'][0]; - // data shouldn't be altered - $this->assertEquals($str, $data['title']); - $this->assertEquals($str, $link['url']); - - // plugin data - $this->assertArrayNotHasKey('link_plugin', $link); - } -} diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 36d58c68..1f4b3063 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php @@ -56,7 +56,7 @@ class ReferenceLinkDB 0, DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 'gnu media web .hidden hashtag', - null, + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 'IuWvgA' ); diff --git a/tpl/default/configure.html b/tpl/default/configure.html index fd8ee9c2..7469ab59 100644 --- a/tpl/default/configure.html +++ b/tpl/default/configure.html @@ -206,7 +206,7 @@
-
-
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css index 8fcd13af..73fade5f 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css @@ -35,14 +35,29 @@ pre { } @font-face { - font-family: 'Roboto Slab'; + font-family: 'Roboto'; font-weight: 400; font-style: normal; src: - local('Fira Sans'), - local('Fira-Sans-regular'), - url('../fonts/Fira-Sans-regular.woff2') format('woff2'), - url('../fonts/Fira-Sans-regular.woff') format('woff'); + local('Roboto'), + local('Roboto-Regular'), + url('../fonts/Roboto-Regular.woff2') format('woff2'), + url('../fonts/Roboto-Regular.woff') format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-weight: 700; + font-style: normal; + src: + local('Roboto'), + local('Roboto-Bold'), + url('../fonts/Roboto-Bold.woff2') format('woff2'), + url('../fonts/Roboto-Bold.woff') format('woff'); +} + +body, .pure-g [class*="pure-u"] { + font-family: Roboto, Arial, sans-serif; } /** @@ -68,10 +83,6 @@ pre { .pure-u-xl-visible { display: inline-block !important; } } -.pure-g [class*="pure-u"]{ - font-family: Roboto Slab, Arial, sans-serif; -} - /** * Make pure-extras alert closable. */ @@ -504,7 +515,6 @@ pre { color: #252525; text-decoration: none; vertical-align: middle; - font-family: Roboto Slab, Arial, sans-serif; } .linklist-item-title .linklist-link { @@ -560,7 +570,6 @@ pre { .linklist-item-description { position: relative; padding: 10px; - font-family: Roboto Slab, Arial, sans-serif; word-wrap: break-word; color: #252525; line-height: 1.3em; diff --git a/tpl/default/daily.html b/tpl/default/daily.html index d8c91078..29d845d5 100644 --- a/tpl/default/daily.html +++ b/tpl/default/daily.html @@ -44,7 +44,7 @@
-

{function="strftime('%A %d, %B %Y', $day)"}

+

{function="format_date($dayDate, false)"}

{loop="$daily_about_plugin"} diff --git a/tpl/default/fonts/Fira-Sans-regular.woff b/tpl/default/fonts/Fira-Sans-regular.woff deleted file mode 100644 index 014ac317..00000000 Binary files a/tpl/default/fonts/Fira-Sans-regular.woff and /dev/null differ diff --git a/tpl/default/fonts/Fira-Sans-regular.woff2 b/tpl/default/fonts/Fira-Sans-regular.woff2 deleted file mode 100644 index bf3ad9a4..00000000 Binary files a/tpl/default/fonts/Fira-Sans-regular.woff2 and /dev/null differ diff --git a/tpl/default/fonts/Roboto-Bold.woff b/tpl/default/fonts/Roboto-Bold.woff new file mode 100644 index 00000000..3d86753b Binary files /dev/null and b/tpl/default/fonts/Roboto-Bold.woff differ diff --git a/tpl/default/fonts/Roboto-Bold.woff2 b/tpl/default/fonts/Roboto-Bold.woff2 new file mode 100644 index 00000000..bd05e2ea Binary files /dev/null and b/tpl/default/fonts/Roboto-Bold.woff2 differ diff --git a/tpl/default/fonts/Roboto-Regular.woff b/tpl/default/fonts/Roboto-Regular.woff new file mode 100644 index 00000000..464d2062 Binary files /dev/null and b/tpl/default/fonts/Roboto-Regular.woff differ diff --git a/tpl/default/fonts/Roboto-Regular.woff2 b/tpl/default/fonts/Roboto-Regular.woff2 new file mode 100644 index 00000000..f9661967 Binary files /dev/null and b/tpl/default/fonts/Roboto-Regular.woff2 differ diff --git a/tpl/default/import.html b/tpl/default/import.html index e6e521e8..1f040685 100644 --- a/tpl/default/import.html +++ b/tpl/default/import.html @@ -18,6 +18,7 @@
+


Maximum size allowed: {$maxfilesizeHuman}

diff --git a/tpl/default/install.html b/tpl/default/install.html index c5052a26..99aca193 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html @@ -7,6 +7,8 @@ {$ratioLabel='1-4'} {$ratioInput='3-4'} +{$ratioLabelMobile='7-8'} +{$ratioInputMobile='1-8'}
@@ -118,6 +120,22 @@
+
+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js index 714420b7..4d47fcd0 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js @@ -258,10 +258,9 @@ window.onload = function () { * Remove CSS target padding (for fixed bar) */ if (location.hash != '') { - var anchor = document.querySelector(location.hash); + var anchor = document.getElementById(location.hash.substr(1)); if (anchor != null) { var padsize = anchor.clientHeight; - console.log(document.querySelector(location.hash).clientHeight); this.window.scroll(0, this.window.scrollY - padsize); anchor.style.paddingTop = 0; } diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html index 61907ddf..7adc7545 100644 --- a/tpl/vintage/configure.html +++ b/tpl/vintage/configure.html @@ -117,9 +117,9 @@ Enable REST API - - + diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html index 071e1160..bb9e4a56 100644 --- a/tpl/vintage/import.html +++ b/tpl/vintage/import.html @@ -5,7 +5,7 @@