Skip to content


A little catch when submitting an iOS app with Facebook SDK

When you submit your app to Apple, at the end of the process they ask you if the app uses the iOS Advertising Identifier. As far as I understood, the Facebook SDK does use this identifier, so if you want to avoid wasting a week because of a rejected submission, you should declare that you use it. More specifically:

This app uses the Advertising Identifier to (select all that apply)?
– Serve advertisements within the app
– Attribute this app installation to a previously served advertisement
– Attribute an action taken within this app to a previously served advertisement

If you will be using the Audience Network framework, you must select the first option.
If you are using our core framework to track install attribution and app events, please select the second and third options.
If you are using both, select all three.

(source: https://developers.facebook.com/bugs/242477629268301/)

In the case of the app I’ve been working on, we track install attribution (to check of an ad on Facebook led to an app installation), so option 2 and 3.

Another interesting read (for historical purpose!):
http://stackoverflow.com/questions/21574680/app-rejected-because-of-advertisingidentifier-in-facebook-sdk-and-flurry-sdk

On a side note, Facebook’s tool to see if your app seems properly configured:
https://developers.facebook.com/tools/app-ads-helper/

Posted in programming, Xcode.


Going HTTPS (finally) with Let’s Encrypt

I’ve long used a CAcert certificate to provide secure access to this site, as I’m on a budget since the ads barely pay for 1% of the hosting. However, sadly CAcert certificates aren’t accepted by browsers without a warning first, so this remained a kind of hidden option. Now that Let’s Encrypt is getting mainstream enough, it was time to finally get a “proper” SSL/TLS certificate.

I will list here the commands I used for setting up my first Let’s Encrypt certificate, which I added to my picture gallery a couple of weeks ago. It’s actually extremely close to what’s listing in Let’s Encrypt’s “Getting Started” guide. Note that as I’m using a distribution which doesn’t have a letsencrypt package, I’m using the compile-it-yourself version, but as you’ll see it’s still very easy (only it takes up more space because you need to install Git).

First, install Git if you don’t already have it:
apt-get install git

Now go to a folder where you want to place Let’s Encrypt (anywhere you want, as long as it’s not exposed to the world – ie don’t put this into your web-facing folders), for instance:
cd /home/mycertifs

Then clone letsencrypt:
git clone https://github.com/letsencrypt/letsencrypt

View the help if you want to (NB: at this moment, it will check for update and install):
./letsencrypt-auto --help
You see we use ./letsencrypt-auto, it’s because we use the manually installed version. If you are lucky enough to have a distro package, the command will be instead just letsencrypt

Then to create the certificate:
./letsencrypt-auto certonly --webroot -w /home/www/gal -d gal.patheticcockroach.com
This will request a certificate for gal.patheticcockroach.com and check domain ownership by placing a verification file in /home/www/gal (make sure that’s where the webserver can be reached from the world). The script takes care of generating a server private key, etc.
The first time you run the script, it will also ask for an e-mail to be used in case of issue/recovery (be sure to enter a real one!).

When all is done, the script will output generic advice + some info on your newly created certificate. Here’s a verbatim:

IMPORTANT NOTES:
 - If you lose your account credentials, you can recover through
   e-mails sent to [the e-mail you entered].
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/gal.patheticcockroach.com/fullchain.pem. Your
   cert will expire on 2016-06-26. To obtain a new version of the
   certificate in the future, simply run Let's Encrypt again.
 - Your account credentials have been saved in your Let's Encrypt
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Let's
   Encrypt so making regular backups of this folder is ideal.
 - If you like Let's Encrypt, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

As a bonus, here is how I configured my Apache HTTPd 2.2 virtual host. There are probably better settings for SSLCipherSuite, but as of today they still get an A at the SSL Labs HTTPS tester and at HT Bridge’s too. So most likely that should be good enough for you (and if it’s not, I’m pretty sure that then you have the budget for an EV certificate 😉 ):

<VirtualHost *:443>
   ServerName gal.patheticcockroach.com
   DocumentRoot "/home/www/gal/"
   <Directory "/home/www/gal/">
   allow from all
   Options -Indexes
   </Directory>
   SSLEngine on
   SSLProtocol all -SSLv2 -SSLv3
   SSLHonorCipherOrder On
   SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:HIGH:!CAMELLIA:!RC4:!MD5:!aNULL:!EDH
   SSLCertificateFile /etc/letsencrypt/live/gal.patheticcockroach.com/cert.pem
   SSLCertificateKeyFile /etc/letsencrypt/live/gal.patheticcockroach.com/privkey.pem
   SSLCertificateChainFile /etc/letsencrypt/live/gal.patheticcockroach.com/fullchain.pem
   SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
</VirtualHost>

Last but not least, when the time comes to renew, you can run a simulation using this:
./letsencrypt-auto renew --dry-run
and renew for real using this:
./letsencrypt-auto renew

Note that I configured my virtual host manually, but I believe there is a way to configure it automatically. I’m not a big fan of letting a script do some unknown magic to my stuff unless it’s really required, and since the virtual host thing is one time only (for renewals, you should be able to keep the very same configuration), doing it by hand made sense to me.

Posted in security, servers, web development.


How to fix Android Studio 2.0 file permission issues

Error:Execution failed for task ':app:clean'.
Unable to delete file (or folder): [some file saved as magical admin]

You are probably here because you encountered the above-mentioned error in Android Studio, most likely on Windows and most likely with version 2.0+. A variant to this is a write error or an unzip error (Android Studio failing to extract some .aar archive because… it couldn’t clear the target folder first).
I tried several things:

  • FileASSASSIN, which tries different techniques to delete a file. It didn’t work but was able to tell me that the file was locked by an unknown program that wasn’t running anymore (at least that how I interpreted that “FileHandle”, if I recall correctly, was null)
  • LockHunter, which tries to unlock files (ditto, it failed to remove the file too)
  • and finally a working solution: disable Android Studio’s new feature “Instant Run” (search for “Instant Run” in settings and uncheck the box). And then restart (the computer, not just the Studio). The “funny” thing is that bug has been reported… for Linux on an NTFS partition

Another option, which I’ve used in the past on similar cases as a last resort, is to reboot into Linux and deletes the files from there. That’s actually my primary reason for still having a dual boot at the moment…

Posted in Android.


aToad #19: script to encode/decode a VBS

Bummer, just got myself a little virus while cleaning up a USB key a little carelessly :s
Most anti-viruses are unable to catch it (virustotal reported about 3 to 6 detections out of 56 anti-viruses, depending on the file), probably because it’s not that nasty. All it seems to do is copy itself into D:\$RECYCLEBIN (note that this is just slithly different from the real D:\$RECYCLE.BIN folder). And to USB keys (and you might want to check other external storage devices, too). And of course add itself to startup via wscript.exe. More specifically it runs this at the end:
WshShell.RegWrite "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run\VideoLAN","C:\WINDOWS\system32\wscript.exe /e:VBScript.Encode D:\$RECYCLEBIN\Vlc.rar","REG_SZ"
WshShell.RegWrite "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run\C-cleaner","C:\WINDOWS\system32\wscript.exe /e:VBScript.Encode D:\$RECYCLEBIN\Adobe.rar","REG_SZ"

It also tries to create new links to your browser(s), so that when you launch the browsers using those links it will open up some page of the site chercheztout[dot]com. Yes, apparently it’s all about getting a few bucks out of Google Search, geez…

These Vlc.rar, Skype.rar and Adobe.rar are all actually encoded VBS files. And here comes the tool this post is about: this script by arnavsharma on Microsoft TechNet was very useful to me, as I was able to decode all files after simply changing their extension from .rar to .vbe.

Last but not least, to delete this virus, all I had to do was:
– remove the run entries
– remove the files from D:\$RECYCLE.BIN
– remove the files from the USB key (both in $RECYCLE.BIN + some shortcuts called “dossier” and “nouveau dossier”)
– actually, I even formatted the USB key after that, but I don’t believe that was really necessary

Posted in A Tool A Day.


How to reorient a picture with EXIF data using PHP GD

Sorry for the briefness, lack of time strikes again.
In this code snippet, $tmpFile is the path to the image file, and for simplicity’s sake here it’s always a JPG.
We define our own function “custImageFlip” to flip the picture (and I put it at the end picture it’s secondary, but if you want to use the code as-is you’ll need to put it at the top), but if your target is PHP>=5.5, you can just use PHP’s native imageflip.

Code for the switch is hugely based on code by someone in the comments in PHP’s documentation.
Code for the PHP<5.5 implementation to flip the image is from another comment in PHP’s doc.

Last but not least, a great image pack that will allow you to test all those picture rotations and flips: https://github.com/recurser/exif-orientation-examples

$tmpImage = @imagecreatefromjpeg($tmpFile);
$exif = exif_read_data($tmpFile);
//die(json_encode($exif));
if($exif!==false) {
	$ort=1;
	if(isset($exif['Orientation'])) {
		// orientation is usually here
		$ort=$exif['Orientation'];
	} elseif(isset($exif['IFD0']) && isset($exif['IFD0']['Orientation'])) {
		// but apparently it can be here sometimes?
		$ort=$exif['IFD0']['Orientation'];
	}
	switch($ort) {
		case 1: // nothing
			break;
		case 2: // horizontal flip
			$tmpImage=custImageFlip($tmpImage,2);
			break;
		case 3: // 180 rotate left
			$tmpImage=imagerotate($tmpImage,180,0);
			break;
		case 4: // vertical flip
			$tmpImage=custImageFlip($tmpImage,1);
			break;
		case 5: // vertical flip + 90 rotate right
			$tmpImage=custImageFlip($tmpImage,1);
			$tmpImage=imagerotate($tmpImage,-90,0);
			break;
		case 6: // 90 rotate right
			$tmpImage=imagerotate($tmpImage,-90,0);
			break;
		case 7: // horizontal flip + 90 rotate right
			$tmpImage=custImageFlip($tmpImage,2);
			$tmpImage=imagerotate($tmpImage,-90,0);
			break;
		case 8: // 90 rotate left
			$tmpImage=imagerotate($tmpImage,90,0);
			break;
	}
}

// ImageFlip from https://php.net/manual/en/function.imagecopy.php#89658
function custImageFlip($imgsrc,$mode) {
	$width=imagesx($imgsrc);
	$height=imagesy($imgsrc);

	$src_x=0;
	$src_y=0;
	$src_width=$width;
	$src_height=$height;

	switch ($mode) {
		case '1': //vertical
			$src_y=$height-1;
			$src_height=-$height;
			break;
		case '2': //horizontal
			$src_x=$width-1;
			$src_width=-$width;
			break;
		case '3': //both
			$src_x=$width-1;
			$src_y=$height-1;
			$src_width=-$width;
			$src_height=-$height;
			break;
		default:
			return $imgsrc;
	}
	$imgdest = imagecreatetruecolor ($width,$height);
	if(imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y , $width, $height, $src_width, $src_height)){
		return $imgdest;
	}
	return $imgsrc;
}

Posted in PHP, programming, web development.


aToad #18: HexChat and FileASSASSIN

Respectively, open source (FLOSS) IRC client and tool to delete sticky files on Windows

Again a “Toad” with 2 tools, because there isn’t much to say about them. I installed HexChat today because I needed to join an IRC channel on Freenode and the web-based IRC client I used to use was down. I’ve never been a big fan of IRC because I find it kind of messy so I’ve always stuck to web clients, but I find HexChat easy enough to use and frankly *a lot* faster than a web-based client lagging in the browser.
A few cool features: can be configured as a portable installation (saves settings in program folder instead of the system folders’ mess), can pin servers and channels to auto-connect to them on startup (kind of obvious I suppose, but it’s the kind of thing I didn’t have in the web client…).
HexChat is a desktop client with a GUI, if you’d rather have a console client or something, you might find what you want on this Top 4 open source IRC clients post where I read about it.

The second tool is FileASSASSIN by Malwarebyte, and unfortunately it’s just freeware. It’s a nice little tool to help you delete tough files.
I’ve been doing some Android development lately, and at some point I hit one of those horrible Windows bug where some poorly conceived piece software generates a file that’s impossible to delete and chokes on it itself (and when you try to delete it manually from Windows Explorer, Windows tells you you don’t have permission to do this, even when you try to run the command as Administrator). In this case, it lead to an Android Studio/IntelliJ error looking like “Cannot save file: Cannot delete temporary file ___jb_old___“. That seems to be a long-running joke (the bug was reported over 4 years ago and is actually marked as fixed because now “there is a UI option (File | Settings | Use “safe write”)”), but that’s the kind of joke where I usually waste 10 minutes to reboot the computer into Linux, delete the file from there, and then boot back into Windows.
FileASSASSIN tries a few tricks to unlock the file and tries to delete it. In my case it failed to delete the file directly, but it also offers to mark the file for deletion at reboot, which worked fine. So I still had to reboot, but skipped the boot into Linux part (also convenient if you don’t want to have a dual boot just to handled such annoyances…).
If it doesn’t work out for you, maybe try one of the many other file deletion tools listed in this post.

Posted in A Tool A Day, Windows.


Dealing with the new permission system in Android 6

My crash monitor (ACRA with Tracepot hosting) recently caught a crash saying something along those lines: “Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 […] requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS”.

Turns out the app wasn’t checking for permission before trying to read contacts and just assumed it was granted, causing a crash when it wasn’t upon calling context.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null);
This issue became obvious with Android 6, but it actually already exists in lower custom versions, provided they have an improve permission system (like CyanogenMod of OxygenOS) and that the user used it to remove the app’s read contact permission.

I found the core solution in this post on Stackoverflow, still I thought I’d share my slightly different implementation. The function to call is tryReadContact(), which will then handle the permission asking, etc

protected void myReadContactFunction() {
	// this is my function which needs to have READ_CONTACTS permission
	// I use a try catch so that it can always run (if permission denied, it returns an empty result instead of crashing)
	Map contactsMap = new HashMap<>();
	ContentResolver cr = c.getContentResolver();
	Cursor cursor=null;
	try {
		cursor = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null);
	} catch(Exception e) {
		// we land here if no permission to read contacts
		Log.v("myReadContactFunction","reading phone contacts denied");
		return contactsMap;
	}
	if (cursor != null) {
		// do read contacts
	}
	return contactsMap;
}

protected void tryReadContact() {
	if(ActivityCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)==PackageManager.PERMISSION_DENIED) {
		ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, READ_CONTACT_REQUEST_CODE);
	} else {
		myReadContactFunction();
	}
}

@Override
public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) {
	// NB: the switch is here to demonstrate support for multiple types of permission requests
	switch (requestCode){
		case READ_CONTACT_REQUEST_CODE:
			if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
				Log.v("PeopleListActivity", "Contact permission has now been granted.");
			} else {
				Log.v("PeopleListActivity", "Contact permission was NOT granted.");
			}
			// in both cases we call myReadContactFunction, since the function reads more than phone contacts and can deal with non-granted permissions
			myReadContactFunction();
			break;
	}
}

The camera permissions are annoying too now. Got this error “android M java.lang.SecurityException: Permission Denial: starting Intent […] android.media.action.IMAGE_CAPTURE […] with revoked permission android.permission.CAMERA”.
We could just do the very same thing as above, but the Android documentation recommends to use intents rather than asking for permission whenever it is appropriate, and it was the case for me there. In such case, you must NOT declare the permission related to the intent, or if you do then you still have to request the permission: “if you app targets M and above and declares as using the CAMERA permission which is not granted, then attempting to use [ACTION_IMAGE_CAPTURE] will result in a SecurityException” (source ACTION_IMAGE_CAPTURE documentation). The reason for this makes sense, this is to avoid user confusion on an app appearing to be able to use the camera even after being denied the camera permission (source).
So basically, in my case all I had to do was to remove <uses-permission android:name="android.permission.CAMERA"/> from AndroidManifest.xml, and just call my new Intent(MediaStore.ACTION_IMAGE_CAPTURE) as I did before.

Posted in Android, programming.


How to remove Google from Opera Speed Dial

So I finally upgraded Opera, as even though version 12 was nice, Presto got definitely rusty. It shockingly lacks tons of customizations that used to be its strength, but I guess it’s still better than Chrome.

Something annoying is that they hid some settings. Namely, there are 2 kinds of hidden settings: the “advanced” settings and the “power user” settings. To unlock the advanced settings, go to Settings => Browser, and then scroll to the bottom and check the “Show advanced settings” box. This enables options such as restoring a real, standalone search bar and showing the full URLs in the address bar.

But for something as trivial as getting rid of Google, you need to access the “power user” settings. And they hid it really well, you actually need to type a freaking code to access it: when you are still in settings, type the following keys: Up Up Down Down Left Right Left Right B A. If you did it right, you’ll get a popup to confirm that you want to unlock those settings. That popup may sound a bit scary for instance they say “These settings are hidden for a reason. They can severely mess up the browser and we can take no responsibility for the stability or performance of the product after proceeding”. But this seems to be pretty BS: I only found TWO “power user” settings, one of which being the famous checkbox to hide the search box in speed dial (yes you can remove the Google serch box but you can not replace it with another search engine, thanks for the anti-feature Opera).

While you’re there, maybe don’t forget to check the “Always show power user settings” box, otherwise you’ll need to type the code every time you want to access them.

Posted in Opera.


How to modify date and time of old commits in Git

Sorry if this post is a bit abrupt / lacks details, it was really written as a quick memo for myself. You will find plenty more details in the sources listed at the bottom of the post (although most of them are simply what shows up first in DuckDuckGo when you search for how to edit old commits).

For this example, we will edit the last 10 commits:

git rebase --interactive HEAD~10

NB: if you’re using SourceTree, you can open a console to Git via the Menu: Actions => Open in Terminal

This will open a text editor with a listing of commits to process, like:

edit e1186e8 Oldest commit
edit 830e58c ANother commit
edit 9d41af7 Latest commit description

By default on Windows, for me the editor was that HORRIBLE shit called Vim. To start editing the text, press “Insert” key. Change the pick keyword to edit for the commits you want to modify. When you’re done, to save and quit press “Escape” (this leaves text editing mode), then type “:wq” and hit “Enter”.

Rebase will then process, telling you “You can amend the commit now”. To change the date without opening an editor (the editor would allow you to edit the commit text I think), use:

git commit --amend --date="20160101T12:12:12" --no-edit

(NB: for supported date formats, see here)

If you’re happy with your change, you can go on to the next commit with

git rebase --continue

Once you’ve run this command on the last commit, your branch will be updated (“Successfully rebased and updated refs/heads/master.”). If you screw up, git rebase --abort should revert all your changes (which seem to only get applied once you’re done with the last commit).

Last but not least, there seems to be a better way to edit the date, using something like this:

GIT_COMMITTER_DATE="20160101T12:12:12" git commit --amend

But I haven’t tried this yet.

A bunch of sources that helped (even though they all had big flaws that still made this a headache):
https://stackoverflow.com/questions/3042437/change-commit-author-at-one-specific-commit
https://stackoverflow.com/questions/9110310/update-git-commit-author-date-when-amending
https://gist.github.com/maciej/5875828
https://help.github.com/articles/about-git-rebase/
https://stackoverflow.com/questions/454734/how-can-one-change-the-timestamp-of-an-old-commit-in-git
http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html

PS: a little bonus: to “uncommit” the last commit (this cancels the commit but leaves the commit + the content of the commit is still selected and ready to be committed again): reset --soft HEAD^

PS2: if you try to run rebase while still having uncommitted changes, you’ll get an error saying “Cannot pull with rebase: You have unstaged changes. Please commit or stash them.” As the message says, you can commit your changes to bypass this, but if you don’t want to commit, you can run “git stash”, then do the rebase procedure, then at the end run “git stash pop”. Cf this stackoverflow post.

Posted in programming.


Migrating from Gallery3 to Piwigo

As I posted a while back, they killed Gallery3. And finally I found the time to move (also I was getting annoyed of restraining myself from posting more pictures on the outdated software). Here are some pointers I would have liked to have for the migration :

First, in gallery3 it was okay to drop pictures in no album, in the root folder. In Piwigo, I believe it is NOT possible, and at least the migration script will ignore pictures left in the root folder. So before you make the switch, you should create a new album and put the pictures from the root folder into it. Or just take the time to finally sort those pictures 😉
While you’re there, check if you don’t have picture file names with quotes in them, as Piwigo don’t support that (and anyway you just shouldn’t use that kind of characters in file names).

After that, it’s mostly straightforward:
– download Piwigo. I used the “package” but I suppose the “webinstall” works just as well
– install it following the instructions there
– once Piwigo is installed go to the administration panel, then to Plugins > Manage, then to the “Other plugins available” tab and install the plugin “Menalto2Piwigo”
– once Menalto2Piwigo is installed, go to Plugins > Menalto2Piwigo, and follow the instructions:
* step 1) Copy the content of g2data/albums (Gallery2) or var/albums (Gallery3) into [your piwigo folder]/galleries
* step 2) Go to Tools > Synchronize. At this step, you can have an error “PWG-UPDATE-1” when trying to import some files. It is caused by “forbidden” characters in file names, like spaces. Fortunately, the list of allowed characters can be configured: to do so, copy include/config_default.inc.php as local/config/config.inc.php, then locate $conf['sync_chars_regex']. It’s a regex of allowed characters, just add there the characters you need. For instance, I just needed to add spaces so my new line is $conf['sync_chars_regex'] = '/^[a-zA-Z0-9-_. ]+$/';. See this forum thread if you need more details.
* step 3) Submit the form on the page: means obviously you need to put the parameters of your gallery3 database in there
* step 4) Install and activate plugins Extended Description: I’m not sure how useful it is but I did it anyway and it doesn’t seem to do any harm.

And voilà, migration complete, although you might want to check for corrupted characters if your database encodings don’t match (my Gallery3 was non-UTF-8 while my Piwigo is UTF-8, I lost quite a few French characters in the move :().

A little complement now: I had some ads and stat tracking (Piwik) code on my Gallery 3, here is how I added them back. Also by default Piwigo will expose your e-mail address on your gallery, not cool so we’ll deal with that too.

I put my ad code in the header, and actually I didn’t need to edit code for these as I found the subheader was a decent place to put them. In the admin panel, go to Configuration > Options, and then in the first tab you have a “Page banner” section where you can put HTML code, like:
<iframe data-aa='348' src='//ad.a-ads.com/348' scrolling='no' style='width:320px;height:50px;border:0px;padding:0;overflow:hidden;margin-left:20px;' allowtransparency='true'></iframe>
<iframe data-aa='348' src='//ad.a-ads.com/348' scrolling='no' style='width:320px;height:50px;border:0px;padding:0;overflow:hidden' allowtransparency='true'></iframe>

If you prefer to move it around a bit, you can probably find a good fit placing your ad code directly inside themes/default/template/header.tpl.

I put my stat tracking code in the footer, and that’s where the e-mail address is too. The file to edit is themes/default/template/footer.tpl.
For the e-mail, locate the section with “if isset($CONTACT_MAIL)”, and comment the block you want to remove with {* (that’s the comment start) and *} (that’s the comment end). NB: note the comment above saying you shouldn’t remove the “Powered by Piwigo” part, so make sure you don’t comment out too much!
For the stat tracking, you know the drill: just before </body>, as usual, so you can put your code just a few lines above the end of the file.
Last note: we edited the default theme, which means if you configure your gallery to use another theme, well you need to edit that theme instead.

…and that’s all, congrats on moving! I must say I found gallery3 smoother (even though I’m definitely not a design addict), but sticking to unmaintained software is bad, so…

Update: many customizations can be done using plugins

@plegall on Twitter pointed out that most of my above customizations can be done without touching the code but by using plugins:
– to remove the e-mail in footer, plugins “Contact Form” + “Protect Notification”
– to add Piwik or *cough* Google Analytics stats, plugin “Statistics”
– to add the regex to the config file, plugin “LocalFiles Editor”, and also note that, of course, you can limit the content of your modified config.inc.php to the values that you did modify

Posted in multimedia, open source.