Dostęp do webservice przez PHP

Dostęp do webservice przez PHP
0

GUS udostępnił nam coś takiego: TERYT

Wysłałem maila, przysłali mi dane logowania z linkami do WSDL i SVC. Jest tylko jeden problem - przykład, jaki podali na połączenie się z tą usługą jest w C#. Oto on:

Kopiuj
try {
	var proxy = new ChannelFactory < ServiceReferenceWCF.ITerytWs1 > ("custom");
	proxy.Credentials.UserName.UserName = login;
	proxy.Credentials.UserName.Password = haslo;
	var result = proxy.CreateChannel();
	var test = result.CzyZalogowany();
} catch (Exception ex) {}

Dali też "Przykładowy fragment pliku konfiguracyjnego":

Kopiuj
<client>
	<endpoint address="https://host/TerytWs1.svc" binding="customBinding" bindingConfiguration="custom" contract="ServiceReference1.ITerytWs1" name="custom" />
</client>

<bindings>
    <customBinding>
    	<binding name="custom">
    		<security defaultAlgorithmSuite="Default" authenticationMode="UserNameOverTransport" requireDerivedKeys="true" includeTimestamp="true" messageSecurityVersion="WSSecurity11WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
    			<localClientSettings detectReplays="false" />
    			<localServiceSettings detectReplays="false" />
    		</security>
    		<textMessageEncoding messageVersion="Soap11WSAddressing10" />
    		<httpsTransport maxReceivedMessageSize="2147483647" maxBufferPoolSize="2147483647" />
    	</binding>
    </customBinding>
</bindings>

Próbowałem na wszelkie sposoby połączyć się z tym przez SOAP, niestety bez powodzenia. Poniżej to, co już próbowałem:

Kopiuj
$wsdl_proto = 'https';
$wsdl_host = 'host';
$wsdl_host_path = '/terytws1.wsdl';
$wsdl_url = $wsdl_proto.'://'.$wsdl_host.$wsdl_host_path;
$connectionPG = new SoapClient($wsdl_url, array(
	'login' => "login",
	'password' => "haslo",
	'trace' => 1,
));

try{
	$logged_in = $connectionPG->CzyZalogowany();
} catch (SoapFault $fault) {
	echo $fault->getMessage()."\n\n";
	echo "REQUEST HEADERS:\n" . $connectionPG->__getLastRequestHeaders() . "\n";
	echo "REQUEST:\n" . $connectionPG->__getLastRequest() . "\n";
	echo "Response headers:\n" . $connectionPG->__getLastResponseHeaders() . "\n";
	echo "Response:\n" . $connectionPG->__getLastResponse() . "\n";
}
$functions = $connectionPG->__getFunctions();
var_dump($functions);

Skrypt zawiesza się podczas próby wykonania funkcji CzyZalogowany(). Kiedy przekroczy timeout, wchodzi do sekcji catch. Response headers są puste. Kombinowałem z ustawieniem parametru location. Doszedłem do momentu, w którym dostałem odpowiedź, że metoda POST nie jest dopuszczalną metodą odwoływania się do tego pliku svc (dostępna była m.in GET). Ściągnąłem SoapUI. Tam udało mi się zajść jeszcze dalej. W odpowiedzi otrzymywałem błąd An error occurred when verifying security for the message.
No i na tym sprawa stanęła. Ma ktoś jakiś pomysł jak do tego podejść?

LN
  • Rejestracja:ponad 9 lat
  • Ostatnio:ponad 3 lata
  • Lokalizacja:Polska
  • Postów:14
0

Trudno powiedzieć co się dzieje. SOAP w PHP ma sporo błędów które są poprawiane z wersji na wersję. Spróbuj przechwycić kopertę w metodzie __doRequest() i zobacz co w rzeczywistości wysyłasz. Możesz także spróbować zobaczyć tcpdumpem co wysyłasz i dostajesz w odpowiedzi.

Nie wiem co ten webserwis daje poza dostępem do bazy, ale może lepszym rozwiązaniem byłoby przechowywanie bazy TERYT lokalnie. W projekcie nad którym pracuję właśnie tak robimy. Możesz sobie zobaczyć jak to mamy zrobione: https://github.com/lmsgit/lms, należy przeszukać repozytorium szukając "teryt" lub "location", a w https://github.com/lmsgit/lms/blob/master/bin/lms-teryt jest zrobione zaciąganie bazy danych, mergowanie z istniejącymi adresami.

0
LionNet napisał(a):

Trudno powiedzieć co się dzieje. SOAP w PHP ma sporo błędów które są poprawiane z wersji na wersję. Spróbuj przechwycić kopertę w metodzie __doRequest() i zobacz co w rzeczywistości wysyłasz. Możesz także spróbować zobaczyć tcpdumpem co wysyłasz i dostajesz w odpowiedzi.

Nie wiem co ten webserwis daje poza dostępem do bazy, ale może lepszym rozwiązaniem byłoby przechowywanie bazy TERYT lokalnie. W projekcie nad którym pracuję właśnie tak robimy. Możesz sobie zobaczyć jak to mamy zrobione: https://github.com/lmsgit/lms, należy przeszukać repozytorium szukając "teryt" lub "location", a w https://github.com/lmsgit/lms/blob/master/bin/lms-teryt jest zrobione zaciąganie bazy danych, mergowanie z istniejącymi adresami.

Masz rację, zdecydowanie lepszym rozwiązaniem jest przechowywanie bazy TERYT lokalnie - w przypadku awarii webservice zawsze będzie możliwość zapytania lokalnej bazy. Mimo wszystko chciałbym rozwiązać ten problem. Na StackOverflow zasugerowali, że problem leży w kodowaniu wiadomości - plik konfiguracyjny XML wskazuje na kodowanie WS-Addressing. Domyślnia klasa SoapClient nie jest w stanie korzystać z WS-Addressing, trzeba ją rozszerzyć i dodać tę funkcjonalność. Znalazłem kilka gotowców. Np. ten. Jak mi się uda, to napiszę tutaj jak to zrobić.

Grzegorz Kowalski
  • Rejestracja:ponad 7 lat
  • Ostatnio:4 miesiące
  • Lokalizacja:Warszawa
  • Postów:4
0

Wiem, że to stary wątek, ale ponieważ ostatnimi dniami musiałem skomunikować się z TERYT poprzez PHP a chciałem pozostać jak najbliżej oryginalnego SoapClient'a, to trafiłem m.in. tutaj i podejrzewam, że inni tacy jak ja też tutaj trafią - to forum jest pierwsze w wynikach Google.

No więc jak się okazuje problem komunikacji z webservice'ami wymaga zrobienia dwóch rzeczy:

  1. przemianowania namespace'a SOAP-ENV na soapenv;
  2. dodania nagłówka SOAP zgodnego z WS-ADDRESSING, WS-SECURITY i innymi WS.

Rozwiązaniem okazuje się następujące rozszerzenie SoapClient'a, które zamieściłem też u siebie na GitHubie:

Kopiuj
class TERYT_SoapClient extends SoapClient {
  protected $user;
  protected $pass;

  public function __construct ($wsdl, $options) {
    $this->user = $options['ws-security-login'];
    $this->pass = $options['ws-security-password'];
    parent::__construct($wsdl, $options);
  }

  public function __doRequest ($request , $location , $action , $version, $one_way = 0)
  {
    $nonce_encoded = base64_encode( bin2hex( openssl_random_pseudo_bytes( 16 ) ) );
    $header = '
    <SOAP-ENV:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
  		<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  			<wsse:UsernameToken wsu:Id="UsernameToken-'. date('c') .'">
  				<wsse:Username>'. $this->user .'</wsse:Username>
  				<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">'. $this->pass .'</wsse:Password>
  				<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">'. $nonce_encoded .'</wsse:Nonce>
  				<wsu:Created>'. date('c') .'</wsu:Created>
  			</wsse:UsernameToken>
  		</wsse:Security>
  		<wsa:Action>'. $action .'</wsa:Action>
  	</SOAP-ENV:Header>
    ';
    $xml = explode('<SOAP-ENV:Body>', $request);
    $request = str_replace('SOAP-ENV', 'soapenv', $xml[0] . $header . '<SOAP-ENV:Body>' . $xml[1]);
    return parent::__doRequest($request , $location, $action, $version, $one_way);
  }
}

W opcjach przekazywanych do SoapClient'a należy odpowiednio wypełnić ws-security-login oraz ws-security-password.

edytowany 1x, ostatnio: Grzegorz Kowalski
1

cenna wskazówka, ale niestety nie działa u mnie :(
czy mógłby Pan podać przykład wysłanej (i działającej) koperty ?
chodzi mi głównie o parametry username i password
czy przekazuje je Pan zwykłym tekstem ?
zgonie z
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
hasło powinno być zakodowane wg schematu
Base64 ( SHA-1 ( nonce + created + password ) )
w pana objaśnieniu nie jest jasne co Pan przekazuje
niestety, czy przekazuję zwykły tekst, czy zakodowany - ten sam komunikat "500 internal server error"
z poważaniem
Piotr Gołębiewski

Grzegorz Kowalski
  • Rejestracja:ponad 7 lat
  • Ostatnio:4 miesiące
  • Lokalizacja:Warszawa
  • Postów:4
0
pgoleb napisał(a):

cenna wskazówka, ale niestety nie działa u mnie :(

A co konkretnie nie działa? Jaki jest komunikat błędu i czego się tyczy? Kod, który przedstawiłem wyżej i który też jest w moim repo na GitHubie sprawdzałem przed sekundą i działał jak należy. Skutecznie wywołałem odpowiednią metodę API, aby uzyskać dane o jednostkach podziału terytorialnego.

czy mógłby Pan podać przykład wysłanej (i działającej) koperty ?

Proszę bardzo, przed chwilą wygenerowana i działająca:

Kopiuj
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tempuri.org/">
    <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
  		<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  			<wsse:UsernameToken wsu:Id="UsernameToken-2018-01-11T00:16:02+00:00">
  				<wsse:Username>TestPubliczny</wsse:Username>
  				<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">1234abcd</wsse:Password>
  				<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">NWE5MTJkOWEyZTMyMDhhYWM0YWY2YTVmMmNjM2I0NDQ=</wsse:Nonce>
  				<wsu:Created>2018-01-11T00:16:02+00:00</wsu:Created>
  			</wsse:UsernameToken>
  		</wsse:Security>
  		<wsa:Action>http://tempuri.org/ITerytWs1/PobierzSlownikRodzajowJednostek</wsa:Action>
  	</soapenv:Header>
    <soapenv:Body><ns1:PobierzSlownikRodzajowJednostek/></soapenv:Body></soapenv:Envelope>

chodzi mi głównie o parametry username i password
czy przekazuje je Pan zwykłym tekstem ?

Tak.

zgonie z
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
hasło powinno być zakodowane wg schematu
Base64 ( SHA-1 ( nonce + created + password ) )

To dotyczy przekazywania hasła typu wsse:PasswordDigest a ja przekazuję wsse:PasswordText. Nie pamiętam już czemu tak, ale wydaje mi się, że serwer mógł nie wspierać składowania nonce czy coś takiego. A może po prostu nie miałem do tego cierpliwości.

w pana objaśnieniu nie jest jasne co Pan przekazuje

Racja, nie jest to jasne. Mam nadzieję, że teraz już wyjaśniłem i wszystko Ci się powiedzie. W razie wątpliwości zajrzyj do mojego repo. Podstawowe użycie mojego kodu z GitHuba jest takie:

Kopiuj
require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

try {
	$result = $webservice->division_types();
	var_dump($result);
} catch (SoapFault $exception) {
	var_dump($exception);
}

Konstruktor od TERYT_Webservices odpowiednio konfiguruje Soap Clienta i m.in. przekazuje mu dane dostępowe czystym tekstem:

Kopiuj
  public function __construct($user, $pass, $instance = 'production', $trace = false)
  {
    if ($instance == 'production')  $wsdl = 'https://uslugaterytws1.stat.gov.pl/wsdl/terytws1.wsdl';
    if ($instance == 'test')        $wsdl = 'https://uslugaterytws1test.stat.gov.pl/wsdl/terytws1.wsdl';

    $soap_options = array(
      'ws-security-login'    => $user,
      'ws-security-password' => $pass,
      'soap_version'   => SOAP_1_1,
			'cache_wsdl'     => WSDL_CACHE_MEMORY,
			'encoding'       => 'utf8',
      'keep_alive'     => false,
			'trace'					 => $trace,
			'compression'		 => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP | SOAP_COMPRESSION_DEFLATE,
    );

    $this->soap_client = new TERYT_SoapClient($wsdl, $soap_options);
}

Rozmaite dalsze funkcje i funkcyjki korzystają z Soap Clienta w standardowy sposób.

niestety, czy przekazuję zwykły tekst, czy zakodowany - ten sam komunikat "500 internal server error"

Hmm, trudno wyrokować, ale może to jakiś przejściowy problem po stronie serwerów TERYTu?

0

Fatal error: Uncaught Error: Call to undefined method TERYT_Webservices::division_types()

Czy może Pan podać przykład zastosowania? np. jak wyciągnąć województwa

0

albo ulice w danym mieście?

Grzegorz Kowalski
  • Rejestracja:ponad 7 lat
  • Ostatnio:4 miesiące
  • Lokalizacja:Warszawa
  • Postów:4
0
Krwawy Lew napisał(a):

Fatal error: Uncaught Error: Call to undefined method TERYT_Webservices::division_types()

Wklej swój kod PHP, bez tego nie wiem skąd powyższy problem. Przykład, który podałem w poście wyżej i który wykorzystywał division_types(), testowałem na bieżąco przed zapostowaniem i działał bez problemu. Przetestowałem go też teraz i również zadziałał bez problemu. Uzyskałem w wyniku:

Kopiuj
array(7) {
  [1]=>
  string(13) "gmina miejska"
  [2]=>
  string(13) "gmina wiejska"
  [3]=>
  string(21) "gmina miejsko-wiejska"
  [4]=>
  string(33) "miasto w gminie miejsko-wiejskiej"
  [5]=>
  string(41) "obszar wiejski w gminie miejsko-wiejskiej"
  [8]=>
  string(25) "dzielnice m. st. Warszawy"
  [9]=>
  string(53) "delegatury miast: Kraków, Łódź, Poznań, Wrocław"
}

Czy może Pan podać przykład zastosowania? np. jak wyciągnąć województwa

Przykład z województwami jest bliźniaczo podobny do tego z division_types:

Kopiuj
require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

try {
	$result = $webservice->provinces();
	var_dump($result);
} catch (SoapFault $exception) {
	var_dump($exception);
}

W wyniku przed chwilą otrzymałem 16 elementowego arraya, którego tutaj nie wklejam, bo jest obszerny a nie chcę zaciemniać.

Ech, przydałoby się, abym napisał jakąś dokumentację do mojego repo na GitHub. Pierwotnie w tym wątku napisałem jednak nie po to, aby ogólnie zaprezentować wykorzystanie mojego kodu a jedynie sposób na utworzenie SoapClient'a łączącego się z TERYT.

Grzegorz Kowalski
  • Rejestracja:ponad 7 lat
  • Ostatnio:4 miesiące
  • Lokalizacja:Warszawa
  • Postów:4
0
Krwawy Lew napisał(a):

albo ulice w danym mieście?

Teoretycznie poniższy kod powinien to realizować. Nie wiem, czy tak jest, ponieważ testowałem go w tej chwili i otrzymałem zero wyników. Sprawdziłem równolegle webserwisy TERYT za pomocą SoapUI i też otrzymałem zero wyników. Wskazuje to albo na błąd po stronie testowych interfejsów TERYT albo na to, że źle wypełniam request XML do tego konkretnego interfejsu zarówno manualnie (w SoapUI) jak i w sposób zautomatyzowany w kodzie PHP. Być może na interfejsach produkcyjnych wynik byłby inny - nie mam takiego dostępu, aby to sprawdzić.

Kopiuj
require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

	try {
		$towns = $webservice->town_search('Gdańsk');
		foreach ($towns as $town) {
			if ($town->Wojewodztwo == 'POMORSKIE') {
				$result = $webservice->streets(
					$town->WojSymbol,
					$town->PowSymbol,
					$town->GmiSymbol,
					$town->GmiRodzaj,
					$town->Symbol
				);
				var_dump($result);
			} else {
				// To nie ten Gdańsk, o który mi chodzi!
			}
		}
	} catch (SoapFault $exception) {
		var_dump($exception);
	}
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)